Ver Fonte

Merge branch 'master' into imprv/sidebar-skelton-pagetree

kymn há 3 anos atrás
pai
commit
067c04de8c
64 ficheiros alterados com 375 adições e 239 exclusões
  1. 3 0
      packages/app/public/static/locales/en_US/admin.json
  2. 0 1
      packages/app/public/static/locales/en_US/translation.json
  3. 3 0
      packages/app/public/static/locales/ja_JP/admin.json
  4. 0 1
      packages/app/public/static/locales/ja_JP/translation.json
  5. 3 1
      packages/app/public/static/locales/zh_CN/admin.json
  6. 0 1
      packages/app/public/static/locales/zh_CN/translation.json
  7. 1 1
      packages/app/src/components/Admin/App/AppSetting.jsx
  8. 1 1
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  9. 7 7
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  10. 1 1
      packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  11. 1 1
      packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  12. 2 2
      packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  13. 1 1
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  14. 1 1
      packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx
  15. 1 1
      packages/app/src/components/Admin/UserGroup/UserGroupDropdown.tsx
  16. 39 24
      packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  17. 3 7
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  18. 3 3
      packages/app/src/components/AlertSiteUrlUndefined.tsx
  19. 2 1
      packages/app/src/components/Comments.tsx
  20. 2 1
      packages/app/src/components/DescendantsPageList.tsx
  21. 2 1
      packages/app/src/components/Fab.tsx
  22. 1 1
      packages/app/src/components/Hotkeys/Subscribers/CreatePage.jsx
  23. 9 6
      packages/app/src/components/LoginForm.tsx
  24. 2 1
      packages/app/src/components/Navbar/GlobalSearch.tsx
  25. 2 1
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  26. 2 1
      packages/app/src/components/Navbar/GrowiNavbarBottom.tsx
  27. 2 1
      packages/app/src/components/Page.tsx
  28. 2 2
      packages/app/src/components/Page/DisplaySwitcher.tsx
  29. 1 2
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  30. 2 1
      packages/app/src/components/PageComment/CommentEditor.tsx
  31. 2 2
      packages/app/src/components/PageEditor.tsx
  32. 2 1
      packages/app/src/components/PageEditor/EditorNavbarBottom.tsx
  33. 1 1
      packages/app/src/components/PageEditor/LinkEditModal.jsx
  34. 6 6
      packages/app/src/components/PageEditorByHackmd.tsx
  35. 1 1
      packages/app/src/components/PageList/PageListItemL.tsx
  36. 1 1
      packages/app/src/components/PageTimeline.tsx
  37. 1 1
      packages/app/src/components/RevisionComparer/RevisionComparer.tsx
  38. 2 1
      packages/app/src/components/SavePageControls.tsx
  39. 2 1
      packages/app/src/components/Sidebar/PageTree.tsx
  40. 4 8
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  41. 17 6
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  42. 1 1
      packages/app/src/components/TableOfContents.tsx
  43. 3 2
      packages/app/src/components/User/UserDate.jsx
  44. 1 2
      packages/app/src/pages/[[...path]].page.tsx
  45. 2 2
      packages/app/src/pages/admin/app.page.tsx
  46. 9 0
      packages/app/src/pages/admin/index.page.tsx
  47. 15 4
      packages/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx
  48. 1 5
      packages/app/src/pages/installer.page.tsx
  49. 1 2
      packages/app/src/pages/share/[[...path]].page.tsx
  50. 1 2
      packages/app/src/pages/trash.page.tsx
  51. 18 14
      packages/app/src/server/routes/login-passport.js
  52. 57 67
      packages/app/src/stores/context.tsx
  53. 1 1
      packages/app/src/stores/page-listing.tsx
  54. 1 1
      packages/app/src/stores/page-redirect.tsx
  55. 52 16
      packages/app/src/stores/page.tsx
  56. 3 1
      packages/app/src/stores/renderer.tsx
  57. 9 2
      packages/app/src/stores/ui.tsx
  58. 24 0
      packages/app/src/stores/use-context-swr.tsx
  59. 9 0
      packages/app/test/cypress/integration/20-basic-features/access-to-page.spec.ts
  60. 6 3
      packages/app/test/cypress/integration/20-basic-features/click-page-icons.spec.ts
  61. 4 4
      packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts
  62. 2 1
      packages/app/test/cypress/integration/21-basic-features-for-guest/access-to-page.spec.ts
  63. 12 6
      packages/app/test/cypress/integration/50-sidebar/access-to-side-bar.spec.ts
  64. 5 1
      packages/app/test/cypress/integration/60-home/home.spec.ts

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

@@ -862,6 +862,9 @@
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
     }
   },
   },
+  "cloud_setting_management": {
+    "to_cloud_settings": "Open GROWI.cloud Settings"
+  },
   "audit_log_action_category": {
   "audit_log_action_category": {
     "Page": "Page",
     "Page": "Page",
     "Comment": "Comment",
     "Comment": "Comment",

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

@@ -625,7 +625,6 @@
       "error_duplicate_pages_found": "Multiple pages with the same path name were found. Please rename or delete and try again."
       "error_duplicate_pages_found": "Multiple pages with the same path name were found. Please rename or delete and try again."
     }
     }
   },
   },
-  "to_cloud_settings": "Open GROWI.cloud Settings",
   "login": {
   "login": {
     "Sign in error": "Login error",
     "Sign in error": "Login error",
     "Registration successful": "Registration successful",
     "Registration successful": "Registration successful",

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

@@ -868,6 +868,9 @@
       "log_type": "https://docs.growi.org/ja/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
       "log_type": "https://docs.growi.org/ja/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
     }
   },
   },
+  "cloud_setting_management": {
+    "to_cloud_settings": "GROWI.cloud の管理画面へ"
+  },
   "audit_log_action_category": {
   "audit_log_action_category": {
     "Page": "ページ",
     "Page": "ページ",
     "Comment": "コメント",
     "Comment": "コメント",

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

@@ -619,7 +619,6 @@
       "error_duplicate_pages_found": "同名のパスを持つページが複数見つかりました。リネームまたは削除してから再度実行してください"
       "error_duplicate_pages_found": "同名のパスを持つページが複数見つかりました。リネームまたは削除してから再度実行してください"
     }
     }
   },
   },
-  "to_cloud_settings": "GROWI.cloud の管理画面へ",
   "login": {
   "login": {
     "Sign in error": "ログインエラー",
     "Sign in error": "ログインエラー",
     "Registration successful": "登録完了",
     "Registration successful": "登録完了",

+ 3 - 1
packages/app/public/static/locales/zh_CN/admin.json

@@ -882,7 +882,9 @@
     "docs_url": {
     "docs_url": {
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
       "log_type": "https://docs.growi.org/en/admin-guide/admin-cookbook/audit-log-setup.html#log-types"
     }
     }
-
+  },
+  "cloud_setting_management": {
+    "to_cloud_settings": "進入 GROWI.cloud 的管理界面"
   },
   },
   "audit_log_action_category": {
   "audit_log_action_category": {
     "Page": "页面",
     "Page": "页面",

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

@@ -627,7 +627,6 @@
       "error_duplicate_pages_found": "发现多个具有相同路径名称的页面。请重新命名或删除并重试。"
       "error_duplicate_pages_found": "发现多个具有相同路径名称的页面。请重新命名或删除并重试。"
     }
     }
   },
   },
-	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",
 	"login": {
 	"login": {
 		"Sign in error": "登录错误",
 		"Sign in error": "登录错误",
 		"Registration successful": "注册成功",
 		"Registration successful": "注册成功",

+ 1 - 1
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -23,7 +23,7 @@ const AppSetting = (props) => {
   const submitHandler = useCallback(async() => {
   const submitHandler = useCallback(async() => {
     try {
     try {
       await adminAppContainer.updateAppSettingHandler();
       await adminAppContainer.updateAppSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('commons:headers.app_settings'), ns: 'commons' }));
+      toastSuccess(t('commons:toaster.update_successed', { target: t('commons:headers.app_settings') }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

+ 1 - 1
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -82,7 +82,7 @@ const AppSettingsPageContents = (props: Props) => {
 
 
       <div className="row">
       <div className="row">
         <div className="col-lg-12">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('commons:headers.app_settings')}</h2>
+          <h2 className="admin-setting-header">{t('headers.app_settings', { ns: 'commons' })}</h2>
           <AppSetting />
           <AppSetting />
         </div>
         </div>
       </div>
       </div>

+ 7 - 7
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -6,7 +6,7 @@ import Link from 'next/link';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-
+import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '../../../stores/context';
 // import AppContainer from '~/client/services/AppContainer';
 // import AppContainer from '~/client/services/AppContainer';
 
 
 // import { withUnstatedContainers } from '../../UnstatedUtils';
 // import { withUnstatedContainers } from '../../UnstatedUtils';
@@ -16,14 +16,14 @@ const AdminNavigation = (props) => {
   // const { appContainer } = props;
   // const { appContainer } = props;
   const pathname = window.location.pathname;
   const pathname = window.location.pathname;
 
 
-  // const growiCloudUri = appContainer.config.env.GROWI_CLOUD_URI;
-  // const growiAppIdForGrowiCloud = appContainer.config.env.GROWI_APP_ID_FOR_GROWI_CLOUD;
+  const { data: growiCloudUri } = useGrowiCloudUri();
+  const { data: growiAppIdForGrowiCloud } = useGrowiAppIdForGrowiCloud();
 
 
   // eslint-disable-next-line react/prop-types
   // eslint-disable-next-line react/prop-types
   const MenuLabel = ({ menu }) => {
   const MenuLabel = ({ menu }) => {
     switch (menu) {
     switch (menu) {
       /* eslint-disable no-multi-spaces, max-len */
       /* eslint-disable no-multi-spaces, max-len */
-      case 'app':                      return <><i className="mr-1 icon-fw icon-settings"></i>{        t('commons:headers.app_settings') }</>;
+      case 'app':                      return <><i className="mr-1 icon-fw icon-settings"></i>{        t('headers.app_settings', { ns: 'commons' }) }</>;
       case 'security':                 return <><i className="mr-1 icon-fw icon-shield"></i>{          t('security_settings.security_settings') }</>;
       case 'security':                 return <><i className="mr-1 icon-fw icon-shield"></i>{          t('security_settings.security_settings') }</>;
       case 'markdown':                 return <><i className="mr-1 icon-fw icon-note"></i>{            t('markdown_settings.markdown_settings') }</>;
       case 'markdown':                 return <><i className="mr-1 icon-fw icon-note"></i>{            t('markdown_settings.markdown_settings') }</>;
       case 'customize':                return <><i className="mr-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;
       case 'customize':                return <><i className="mr-1 icon-fw icon-wrench"></i>{          t('customize_settings.customize_settings') }</>;
@@ -36,7 +36,7 @@ const AdminNavigation = (props) => {
       case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
       case 'user-groups':              return <><i className="mr-1 icon-fw icon-people"></i>{          t('user_group_management.user_group_management') }</>;
       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 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
       case 'audit-log':                return <><i className="mr-1 icon-fw icon-feed"></i>{            t('audit_log_management.audit_log')}</>;
-      case 'cloud':                    return <><i className="mr-1 icon-fw icon-share-alt"></i>{       t('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_home_page') }</>;
       /* eslint-enable no-multi-spaces, max-len */
       /* eslint-enable no-multi-spaces, max-len */
     }
     }
@@ -92,7 +92,7 @@ const AdminNavigation = (props) => {
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="user-groups"  isListGroupItems isActive={isActiveMenu('/user-groups')} />
         <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
         <MenuLink menu="audit-log"    isListGroupItems isActive={isActiveMenu('/audit-log')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
         <MenuLink menu="search"       isListGroupItems isActive={isActiveMenu('/search')} />
-        {/* {growiCloudUri != null && growiAppIdForGrowiCloud != null
+        {growiCloudUri != null && growiAppIdForGrowiCloud != null
           && (
           && (
             <a
             <a
               href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}
               href={`${growiCloudUri}/my/apps/${growiAppIdForGrowiCloud}`}
@@ -101,7 +101,7 @@ const AdminNavigation = (props) => {
               <MenuLabel menu="cloud" />
               <MenuLabel menu="cloud" />
             </a>
             </a>
           )
           )
-        } */}
+        }
         {/* eslint-enable no-multi-spaces */}
         {/* eslint-enable no-multi-spaces */}
       </>
       </>
     );
     );

+ 1 - 1
packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx

@@ -90,7 +90,7 @@ class GitHubSecurityManagementContents extends React.Component {
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}

+ 1 - 1
packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx

@@ -88,7 +88,7 @@ class GoogleSecurityManagementContents extends React.Component {
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}

+ 2 - 2
packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx

@@ -82,7 +82,7 @@ class OidcSecurityManagementContents extends React.Component {
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}
@@ -378,7 +378,7 @@ class OidcSecurityManagementContents extends React.Component {
                     <i
                     <i
                       className="icon-exclamation"
                       className="icon-exclamation"
                       // eslint-disable-next-line max-len
                       // eslint-disable-next-line max-len
-                      dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
+                      dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
                     />
                     />
                   </div>
                   </div>
                 )}
                 )}

+ 1 - 1
packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -99,7 +99,7 @@ class SamlSecurityManagementContents extends React.Component {
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}

+ 1 - 1
packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx

@@ -90,7 +90,7 @@ class TwitterSecuritySettingContents extends React.Component {
                 <i
                 <i
                   className="icon-exclamation"
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('commons:alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('commons:headers.app_settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('alert.siteUrl_is_not_set', { link: `<a href="/admin/app">${t('headers.app_settings', { ns: 'commons' })}<i class="icon-login"></i></a>`, ns: 'commons' }) }}
                 />
                 />
               </div>
               </div>
             )}
             )}

+ 1 - 1
packages/app/src/components/Admin/UserGroup/UserGroupDropdown.tsx

@@ -2,7 +2,7 @@ import React, { FC, useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { IUserGroupHasId } from '~/interfaces/user';
+import type { IUserGroupHasId } from '~/interfaces/user';
 
 
 type Props = {
 type Props = {
   selectableUserGroups?: IUserGroupHasId[]
   selectableUserGroups?: IUserGroupHasId[]

+ 39 - 24
packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -5,6 +5,7 @@ import React, {
 import { objectIdUtils } from '@growi/core';
 import { objectIdUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
+import Link from 'next/link';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
@@ -18,7 +19,7 @@ import { useIsAclEnabled } from '~/stores/context';
 import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
 import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
 import {
 import {
   useSWRxUserGroupPages, useSWRxUserGroupRelationList, useSWRxChildUserGroupList, useSWRxUserGroup,
   useSWRxUserGroupPages, useSWRxUserGroupRelationList, useSWRxChildUserGroupList, useSWRxUserGroup,
-  useSWRxSelectableParentUserGroups, useSWRxSelectableChildUserGroups, useSWRxAncestorUserGroups,
+  useSWRxSelectableParentUserGroups, useSWRxSelectableChildUserGroups, useSWRxAncestorUserGroups, useSWRxUserGroupRelations,
 } from '~/stores/user-group';
 } from '~/stores/user-group';
 
 
 import styles from './UserGroupDetailPage.module.scss';
 import styles from './UserGroupDetailPage.module.scss';
@@ -71,13 +72,14 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
    */
    */
   const { data: userGroupPages } = useSWRxUserGroupPages(currentUserGroupId, 10, 0);
   const { data: userGroupPages } = useSWRxUserGroupPages(currentUserGroupId, 10, 0);
 
 
+  const { data: userGroupRelations, mutate: mutateUserGroupRelations } = useSWRxUserGroupRelations(currentUserGroupId);
 
 
   const { data: childUserGroupsList, mutate: mutateChildUserGroups } = useSWRxChildUserGroupList(currentUserGroupId ? [currentUserGroupId] : [], true);
   const { data: childUserGroupsList, mutate: mutateChildUserGroups } = useSWRxChildUserGroupList(currentUserGroupId ? [currentUserGroupId] : [], true);
   const childUserGroups = childUserGroupsList != null ? childUserGroupsList.childUserGroups : [];
   const childUserGroups = childUserGroupsList != null ? childUserGroupsList.childUserGroups : [];
   const grandChildUserGroups = childUserGroupsList != null ? childUserGroupsList.grandChildUserGroups : [];
   const grandChildUserGroups = childUserGroupsList != null ? childUserGroupsList.grandChildUserGroups : [];
   const childUserGroupIds = childUserGroups.map(group => group._id);
   const childUserGroupIds = childUserGroups.map(group => group._id);
 
 
-  const { data: userGroupRelationList, mutate: mutateUserGroupRelations } = useSWRxUserGroupRelationList(childUserGroupIds);
+  const { data: userGroupRelationList, mutate: mutateUserGroupRelationList } = useSWRxUserGroupRelationList(childUserGroupIds);
   const childUserGroupRelations = userGroupRelationList != null ? userGroupRelationList : [];
   const childUserGroupRelations = userGroupRelationList != null ? userGroupRelationList : [];
 
 
   const { data: selectableParentUserGroups, mutate: mutateSelectableParentUserGroups } = useSWRxSelectableParentUserGroups(currentUserGroupId);
   const { data: selectableParentUserGroups, mutate: mutateSelectableParentUserGroups } = useSWRxSelectableParentUserGroups(currentUserGroupId);
@@ -106,19 +108,19 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
 
 
   const updateUserGroup = useCallback(async(userGroup: IUserGroupHasId, update: Partial<IUserGroupHasId>, forceUpdateParents: boolean) => {
   const updateUserGroup = useCallback(async(userGroup: IUserGroupHasId, update: Partial<IUserGroupHasId>, forceUpdateParents: boolean) => {
     const parentId = typeof update.parent === 'string' ? update.parent : update.parent?._id;
     const parentId = typeof update.parent === 'string' ? update.parent : update.parent?._id;
-    const res = await apiv3Put<{ userGroup: IUserGroupHasId }>(`/user-groups/${userGroup._id}`, {
+    await apiv3Put<{ userGroup: IUserGroupHasId }>(`/user-groups/${userGroup._id}`, {
       name: update.name,
       name: update.name,
       description: update.description,
       description: update.description,
       parentId: parentId ?? null,
       parentId: parentId ?? null,
       forceUpdateParents,
       forceUpdateParents,
     });
     });
-    const { userGroup: updatedUserGroup } = res.data;
 
 
     // mutate
     // mutate
+    mutateChildUserGroups();
     mutateAncestorUserGroups();
     mutateAncestorUserGroups();
     mutateSelectableChildUserGroups();
     mutateSelectableChildUserGroups();
     mutateSelectableParentUserGroups();
     mutateSelectableParentUserGroups();
-  }, [mutateAncestorUserGroups, mutateSelectableChildUserGroups, mutateSelectableParentUserGroups]);
+  }, [mutateAncestorUserGroups, mutateChildUserGroups, mutateSelectableChildUserGroups, mutateSelectableParentUserGroups]);
 
 
   const onSubmitUpdateGroup = useCallback(
   const onSubmitUpdateGroup = useCallback(
     async(targetGroup: IUserGroupHasId, userGroupData: Partial<IUserGroupHasId>, forceUpdateParents: boolean): Promise<void> => {
     async(targetGroup: IUserGroupHasId, userGroupData: Partial<IUserGroupHasId>, forceUpdateParents: boolean): Promise<void> => {
@@ -170,22 +172,28 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
   }, [currentUserGroupId, searchType, isAlsoMailSearched, isAlsoNameSearched]);
   }, [currentUserGroupId, searchType, isAlsoMailSearched, isAlsoNameSearched]);
 
 
   const addUserByUsername = useCallback(async(username: string) => {
   const addUserByUsername = useCallback(async(username: string) => {
-    await apiv3Post(`/user-groups/${currentUserGroupId}/users/${username}`);
-    setIsUserGroupUserModalShown(false);
-    mutateUserGroupRelations();
-  }, [currentUserGroupId, mutateUserGroupRelations]);
+    try {
+      await apiv3Post(`/user-groups/${currentUserGroupId}/users/${username}`);
+      setIsUserGroupUserModalShown(false);
+      mutateUserGroupRelations();
+      mutateUserGroupRelationList();
+    }
+    catch (err) {
+      toastError(new Error(`Unable to add "${username}" from "${currentUserGroup?.name}"`));
+    }
+  }, [currentUserGroup?.name, currentUserGroupId, mutateUserGroupRelationList, mutateUserGroupRelations]);
 
 
   // Fix: invalid csrf token => https://redmine.weseek.co.jp/issues/102704
   // Fix: invalid csrf token => https://redmine.weseek.co.jp/issues/102704
   const removeUserByUsername = useCallback(async(username: string) => {
   const removeUserByUsername = useCallback(async(username: string) => {
     try {
     try {
       await apiv3Delete(`/user-groups/${currentUserGroupId}/users/${username}`);
       await apiv3Delete(`/user-groups/${currentUserGroupId}/users/${username}`);
       toastSuccess(`Removed "${xss.process(username)}" from "${xss.process(currentUserGroup?.name)}"`);
       toastSuccess(`Removed "${xss.process(username)}" from "${xss.process(currentUserGroup?.name)}"`);
-      mutateUserGroupRelations();
+      mutateUserGroupRelationList();
     }
     }
     catch (err) {
     catch (err) {
       toastError(new Error(`Unable to remove "${xss.process(username)}" from "${xss.process(currentUserGroup?.name)}"`));
       toastError(new Error(`Unable to remove "${xss.process(username)}" from "${xss.process(currentUserGroup?.name)}"`));
     }
     }
-  }, [currentUserGroup?.name, currentUserGroupId, mutateUserGroupRelations, xss]);
+  }, [currentUserGroup?.name, currentUserGroupId, mutateUserGroupRelationList, xss]);
 
 
   const showUpdateModal = useCallback((group: IUserGroupHasId) => {
   const showUpdateModal = useCallback((group: IUserGroupHasId) => {
     setUpdateModalShown(true);
     setUpdateModalShown(true);
@@ -319,19 +327,27 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
     <div>
     <div>
       <nav aria-label="breadcrumb">
       <nav aria-label="breadcrumb">
         <ol className="breadcrumb">
         <ol className="breadcrumb">
-          <li className="breadcrumb-item"><a href="/admin/user-groups">{t('user_group_management.group_list')}</a></li>
+          <li className="breadcrumb-item">
+            <Link href="/admin/user-groups" prefetch={false}>
+              <a >{t('user_group_management.group_list')}</a>
+            </Link>
+          </li>
           {
           {
-            ancestorUserGroups != null && ancestorUserGroups.length > 0 && (
-              ancestorUserGroups.map((ancestorUserGroup: IUserGroupHasId) => (
-                // eslint-disable-next-line max-len
-                <li key={ancestorUserGroup._id} className={`breadcrumb-item ${ancestorUserGroup._id === currentUserGroupId ? 'active' : ''}`} aria-current="page">
-                  { ancestorUserGroup._id === currentUserGroupId ? (
-                    <>{ancestorUserGroup.name}</>
-                  ) : (
+            ancestorUserGroups != null && ancestorUserGroups.length > 0 && (ancestorUserGroups.map((ancestorUserGroup: IUserGroupHasId) => (
+              <li
+                key={ancestorUserGroup._id}
+                className={`breadcrumb-item ${ancestorUserGroup._id === currentUserGroupId ? 'active' : ''}`}
+                aria-current="page"
+              >
+                { ancestorUserGroup._id === currentUserGroupId ? (
+                  <span>{ancestorUserGroup.name}</span>
+                ) : (
+                  <Link href={`/admin/user-group-detail/${ancestorUserGroup._id}`} prefetch={false}>
                     <a href={`/admin/user-group-detail/${ancestorUserGroup._id}`}>{ancestorUserGroup.name}</a>
                     <a href={`/admin/user-group-detail/${ancestorUserGroup._id}`}>{ancestorUserGroup.name}</a>
-                  )}
-                </li>
-              ))
+                  </Link>
+                ) }
+              </li>
+            ))
             )
             )
           }
           }
         </ol>
         </ol>
@@ -347,8 +363,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
       </div>
       </div>
       <h2 className="admin-setting-header mt-4">{t('user_group_management.user_list')}</h2>
       <h2 className="admin-setting-header mt-4">{t('user_group_management.user_list')}</h2>
       <UserGroupUserTable
       <UserGroupUserTable
-        userGroup={currentUserGroup}
-        userGroupRelations={childUserGroupRelations}
+        userGroupRelations={userGroupRelations}
         onClickPlusBtn={() => setIsUserGroupUserModalShown(true)}
         onClickPlusBtn={() => setIsUserGroupUserModalShown(true)}
         onClickRemoveUserBtn={removeUserByUsername}
         onClickRemoveUserBtn={removeUserByUsername}
       />
       />

+ 3 - 7
packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx

@@ -4,12 +4,10 @@ import { UserPicture } from '@growi/ui';
 import dateFnsFormat from 'date-fns/format';
 import dateFnsFormat from 'date-fns/format';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { IUserGroupHasId, IUserGroupRelation } from '~/interfaces/user';
-import { useSWRxUserGroupRelations } from '~/stores/user-group';
+import type { IUserGroupRelationHasIdPopulatedUser } from '~/interfaces/user-group-response';
 
 
 type Props = {
 type Props = {
-  userGroupRelations: IUserGroupRelation[],
-  userGroup: IUserGroupHasId,
+  userGroupRelations: IUserGroupRelationHasIdPopulatedUser[] | undefined,
   onClickRemoveUserBtn: (username: string) => Promise<void>,
   onClickRemoveUserBtn: (username: string) => Promise<void>,
   onClickPlusBtn: () => void,
   onClickPlusBtn: () => void,
 }
 }
@@ -18,10 +16,8 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
   const {
   const {
-    userGroup, onClickRemoveUserBtn, onClickPlusBtn,
+    userGroupRelations, onClickRemoveUserBtn, onClickPlusBtn,
   } = props;
   } = props;
-  const { data: userGroupRelations } = useSWRxUserGroupRelations(userGroup._id);
-
 
 
   return (
   return (
     <table className="table table-bordered table-user-list">
     <table className="table table-bordered table-user-list">

+ 3 - 3
packages/app/src/components/AlertSiteUrlUndefined.tsx

@@ -14,7 +14,7 @@ const isValidUrl = (str: string): boolean => {
 };
 };
 
 
 export const AlertSiteUrlUndefined = (): JSX.Element => {
 export const AlertSiteUrlUndefined = (): JSX.Element => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('commons');
   const { data: siteUrl, error: errorSiteUrl } = useSiteUrl();
   const { data: siteUrl, error: errorSiteUrl } = useSiteUrl();
   const isLoadingSiteUrl = siteUrl === undefined && errorSiteUrl === undefined;
   const isLoadingSiteUrl = siteUrl === undefined && errorSiteUrl === undefined;
 
 
@@ -30,8 +30,8 @@ export const AlertSiteUrlUndefined = (): JSX.Element => {
     <div className="alert alert-danger rounded-0 d-edit-none mb-0 px-4 py-2">
     <div className="alert alert-danger rounded-0 d-edit-none mb-0 px-4 py-2">
       <i className="icon-exclamation"></i>
       <i className="icon-exclamation"></i>
       {
       {
-        t('commons:alert.siteUrl_is_not_set', { link: t('commons:headers.app_settings') })
-      } &gt;&gt; <a href="/admin/app">{t('commons:headers.app_settings')}<i className="icon-login"></i></a>
+        t('alert.siteUrl_is_not_set', { link: t('headers.app_settings') })
+      } &gt;&gt; <a href="/admin/app">{t('headers.app_settings')}<i className="icon-login"></i></a>
     </div>
     </div>
   );
   );
 };
 };

+ 2 - 1
packages/app/src/components/Comments.tsx

@@ -5,8 +5,9 @@ import dynamic from 'next/dynamic';
 
 
 import { PageComment } from '~/components/PageComment';
 import { PageComment } from '~/components/PageComment';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useSWRxPageComment } from '~/stores/comment';
+import { useIsTrashPage } from '~/stores/page';
 
 
-import { useIsTrashPage, useCurrentUser } from '../stores/context';
+import { useCurrentUser } from '../stores/context';
 
 
 import { CommentEditorProps } from './PageComment/CommentEditor';
 import { CommentEditorProps } from './PageComment/CommentEditor';
 
 

+ 2 - 1
packages/app/src/components/DescendantsPageList.tsx

@@ -11,8 +11,9 @@ import {
 import { IPagingResult } from '~/interfaces/paging-result';
 import { IPagingResult } from '~/interfaces/paging-result';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import {
 import {
-  useIsGuestUser, useIsSharedUser, useIsTrashPage, useShowPageLimitationXL,
+  useIsGuestUser, useIsSharedUser, useShowPageLimitationXL,
 } from '~/stores/context';
 } from '~/stores/context';
+import { useIsTrashPage } from '~/stores/page';
 import {
 import {
   usePageTreeTermManager, useDescendantsPageListForCurrentPathTermManager, useSWRxDescendantsPageListForCurrrentPath,
   usePageTreeTermManager, useDescendantsPageListForCurrentPathTermManager, useSWRxDescendantsPageListForCurrrentPath,
   useSWRxPageInfoForList, useSWRxPageList,
   useSWRxPageInfoForList, useSWRxPageList,

+ 2 - 1
packages/app/src/components/Fab.tsx

@@ -7,8 +7,9 @@ import { useRipple } from 'react-use-ripple';
 import StickyEvents from 'sticky-events';
 import StickyEvents from 'sticky-events';
 
 
 import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
-import { useCurrentPagePath, useCurrentUser } from '~/stores/context';
+import { useCurrentUser } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
+import { useCurrentPagePath } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { CreatePageIcon } from './Icons/CreatePageIcon';
 import { CreatePageIcon } from './Icons/CreatePageIcon';

+ 1 - 1
packages/app/src/components/Hotkeys/Subscribers/CreatePage.jsx

@@ -2,8 +2,8 @@ import React, { useEffect } from 'react';
 
 
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { useCurrentPagePath } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
+import { useCurrentPagePath } from '~/stores/page';
 
 
 const CreatePage = React.memo((props) => {
 const CreatePage = React.memo((props) => {
 
 

+ 9 - 6
packages/app/src/components/LoginForm.tsx

@@ -263,6 +263,11 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     );
     );
   }, [props, renderExternalAuthInput]);
   }, [props, renderExternalAuthInput]);
 
 
+  const resetRegisterErrors = useCallback(() => {
+    if (registerErrors.length === 0) return;
+    setRegisterErrors([]);
+  }, [registerErrors.length]);
+
   const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
   const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
     e.preventDefault();
     e.preventDefault();
     setEmailForRegistrationOrder('');
     setEmailForRegistrationOrder('');
@@ -276,6 +281,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     };
     };
     try {
     try {
       const res = await apiv3Post(requestPath, { registerForm });
       const res = await apiv3Post(requestPath, { registerForm });
+
+      resetRegisterErrors();
+
       const { redirectTo } = res.data;
       const { redirectTo } = res.data;
       router.push(redirectTo ?? '/');
       router.push(redirectTo ?? '/');
 
 
@@ -291,12 +299,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       }
       }
     }
     }
     return;
     return;
-  }, [emailForRegister, nameForRegister, passwordForRegister, router, usernameForRegister, isEmailAuthenticationEnabled]);
-
-  const resetRegisterErrors = useCallback(() => {
-    if (registerErrors.length === 0) return;
-    setRegisterErrors([]);
-  }, [registerErrors.length]);
+  }, [usernameForRegister, nameForRegister, emailForRegister, passwordForRegister, resetRegisterErrors, router, isEmailAuthenticationEnabled]);
 
 
   const switchForm = useCallback(() => {
   const switchForm = useCallback(() => {
     setIsRegistering(!isRegistering);
     setIsRegistering(!isRegistering);

+ 2 - 1
packages/app/src/components/Navbar/GlobalSearch.tsx

@@ -8,8 +8,9 @@ import { useRouter } from 'next/router';
 import { IFocusable } from '~/client/interfaces/focusable';
 import { IFocusable } from '~/client/interfaces/focusable';
 import { IPageWithSearchMeta } from '~/interfaces/search';
 import { IPageWithSearchMeta } from '~/interfaces/search';
 import {
 import {
-  useCurrentPagePath, useIsSearchScopeChildrenAsDefault, useIsSearchServiceReachable,
+  useIsSearchScopeChildrenAsDefault, useIsSearchServiceReachable,
 } from '~/stores/context';
 } from '~/stores/context';
+import { useCurrentPagePath } from '~/stores/page';
 import { useGlobalSearchFormRef } from '~/stores/ui';
 import { useGlobalSearchFormRef } from '~/stores/ui';
 
 
 import SearchForm from '../SearchForm';
 import SearchForm from '../SearchForm';

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

@@ -11,9 +11,10 @@ import { useRipple } from 'react-use-ripple';
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
 import {
 import {
-  useIsSearchPage, useCurrentPagePath, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useCustomizedLogoSrc,
+  useIsSearchPage, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useCustomizedLogoSrc,
 } from '~/stores/context';
 } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
+import { useCurrentPagePath } from '~/stores/page';
 import { useIsDeviceSmallerThanMd } from '~/stores/ui';
 import { useIsDeviceSmallerThanMd } from '~/stores/ui';
 
 
 import { HasChildren } from '../../interfaces/common';
 import { HasChildren } from '../../interfaces/common';

+ 2 - 1
packages/app/src/components/Navbar/GrowiNavbarBottom.tsx

@@ -1,7 +1,8 @@
 import React from 'react';
 import React from 'react';
 
 
-import { useCurrentPagePath, useIsSearchPage } from '~/stores/context';
+import { useIsSearchPage } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
 import { usePageCreateModal } from '~/stores/modal';
+import { useCurrentPagePath } from '~/stores/page';
 import { useIsDeviceSmallerThanMd, useDrawerOpened } from '~/stores/ui';
 import { useIsDeviceSmallerThanMd, useDrawerOpened } from '~/stores/ui';
 
 
 import { GlobalSearch } from './GlobalSearch';
 import { GlobalSearch } from './GlobalSearch';

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

@@ -13,7 +13,7 @@ import { HtmlElementNode } from 'rehype-toc';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { getOptionsToSave } from '~/client/util/editor';
 import { getOptionsToSave } from '~/client/util/editor';
 import {
 import {
-  useIsGuestUser, useCurrentPageTocNode, useShareLinkId,
+  useIsGuestUser, useShareLinkId,
 } from '~/stores/context';
 } from '~/stores/context';
 import {
 import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
@@ -21,6 +21,7 @@ import {
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import { useViewOptions } from '~/stores/renderer';
 import {
 import {
+  useCurrentPageTocNode,
   useEditorMode, useIsMobile,
   useEditorMode, useIsMobile,
 } from '~/stores/ui';
 } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';

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

@@ -7,10 +7,10 @@ import { Link } from 'react-scroll';
 
 
 import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import {
 import {
-  useCurrentPagePath, useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
+  useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useDescendantsPageListModal } from '~/stores/modal';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 
 
 import CountBadge from '../Common/CountBadge';
 import CountBadge from '../Common/CountBadge';

+ 1 - 2
packages/app/src/components/PageAlert/TrashPageAlert.tsx

@@ -5,9 +5,8 @@ import { format } from 'date-fns';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
-import { useIsTrashPage } from '~/stores/context';
 import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
 import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
-import { useSWRxPageInfo, useSWRxCurrentPage } from '~/stores/page';
+import { useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage } from '~/stores/page';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 
 
 const onDeletedHandler = (pathOrPathsToDelete) => {
 const onDeletedHandler = (pathOrPathsToDelete) => {

+ 2 - 1
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -13,10 +13,11 @@ import { apiPostForm } from '~/client/util/apiv1-client';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { useSWRxPageComment } from '~/stores/comment';
 import { useSWRxPageComment } from '~/stores/comment';
 import {
 import {
-  useCurrentPagePath, useCurrentUser, useRevisionId, useIsSlackConfigured,
+  useCurrentUser, useRevisionId, useIsSlackConfigured,
   useIsUploadableFile, useIsUploadableImage,
   useIsUploadableFile, useIsUploadableImage,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
+import { useCurrentPagePath } from '~/stores/page';
 
 
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import NotAvailableForGuest from '../NotAvailableForGuest';
 import NotAvailableForGuest from '../NotAvailableForGuest';

+ 2 - 2
packages/app/src/components/PageEditor.tsx

@@ -15,14 +15,14 @@ import { apiGet, apiPostForm } from '~/client/util/apiv1-client';
 import { getOptionsToSave } from '~/client/util/editor';
 import { getOptionsToSave } from '~/client/util/editor';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import { IEditorMethods } from '~/interfaces/editor-methods';
 import {
 import {
-  useCurrentPagePath, useCurrentPathname, useCurrentPageId,
+  useCurrentPathname, useCurrentPageId,
   useIsEditable, useIsIndentSizeForced, useIsUploadableFile, useIsUploadableImage, useEditingMarkdown,
   useIsEditable, useIsIndentSizeForced, useIsUploadableFile, useIsUploadableImage, useEditingMarkdown,
 } from '~/stores/context';
 } from '~/stores/context';
 import {
 import {
   useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
   useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
   useIsEnabledUnsavedWarning,
   useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
 } from '~/stores/editor';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
 import { usePreviewOptions } from '~/stores/renderer';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
 import {
   EditorMode,
   EditorMode,

+ 2 - 1
packages/app/src/components/PageEditor/EditorNavbarBottom.tsx

@@ -4,8 +4,9 @@ import dynamic from 'next/dynamic';
 import { Collapse, Button } from 'reactstrap';
 import { Collapse, Button } from 'reactstrap';
 
 
 
 
-import { useCurrentPagePath, useIsSlackConfigured } from '~/stores/context';
+import { useIsSlackConfigured } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
+import { useCurrentPagePath } from '~/stores/page';
 import {
 import {
   EditorMode, useDrawerOpened, useEditorMode, useIsDeviceSmallerThanMd,
   EditorMode, useDrawerOpened, useEditorMode, useIsDeviceSmallerThanMd,
 } from '~/stores/ui';
 } from '~/stores/ui';

+ 1 - 1
packages/app/src/components/PageEditor/LinkEditModal.jsx

@@ -17,7 +17,7 @@ import validator from 'validator';
 
 
 import Linker from '~/client/models/Linker';
 import Linker from '~/client/models/Linker';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
-import { useCurrentPagePath } from '~/stores/context';
+import { useCurrentPagePath } from '~/stores/page';
 
 
 import PagePreviewIcon from '../Icons/PagePreviewIcon';
 import PagePreviewIcon from '../Icons/PagePreviewIcon';
 import SearchTypeahead from '../SearchTypeahead';
 import SearchTypeahead from '../SearchTypeahead';

+ 6 - 6
packages/app/src/components/PageEditorByHackmd.tsx

@@ -13,15 +13,15 @@ import { apiPost } from '~/client/util/apiv1-client';
 import { getOptionsToSave } from '~/client/util/editor';
 import { getOptionsToSave } from '~/client/util/editor';
 import { IResHackmdIntegrated, IResHackmdDiscard } from '~/interfaces/hackmd';
 import { IResHackmdIntegrated, IResHackmdDiscard } from '~/interfaces/hackmd';
 import {
 import {
-  useCurrentPagePath, useCurrentPageId, useHackmdUri,
+  useCurrentPageId, useCurrentPathname, useHackmdUri,
 } from '~/stores/context';
 } from '~/stores/context';
-import {
-  usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useRemoteRevisionId,
-} from '~/stores/hackmd';
 import {
 import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
 } from '~/stores/editor';
-import { useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
+import {
+  usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useRemoteRevisionId,
+} from '~/stores/hackmd';
+import { useCurrentPagePath, useSWRxCurrentPage, useSWRxTagsInfo } from '~/stores/page';
 import {
 import {
   EditorMode,
   EditorMode,
   useEditorMode, useSelectedGrant,
   useEditorMode, useSelectedGrant,
@@ -43,7 +43,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
-  const { data: currentPathname } = useCurrentPagePath();
+  const { data: currentPathname } = useCurrentPathname();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: isSlackEnabled } = useIsSlackEnabled();
   const { data: pageId } = useCurrentPageId();
   const { data: pageId } = useCurrentPageId();

+ 1 - 1
packages/app/src/components/PageList/PageListItemL.tsx

@@ -248,7 +248,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
                   <div dangerouslySetInnerHTML={{ __html: elasticSearchResult.snippet }}></div>
                   <div dangerouslySetInnerHTML={{ __html: elasticSearchResult.snippet }}></div>
                 ) }
                 ) }
                 { revisionShortBody != null && (
                 { revisionShortBody != null && (
-                  <div>{revisionShortBody}</div>
+                  <div data-testid="revision-short-body-in-page-list-item-L">{revisionShortBody}</div>
                 ) }
                 ) }
                 {
                 {
                   !canRenderESSnippet && !canRenderRevisionSnippet && (
                   !canRenderESSnippet && !canRenderRevisionSnippet && (

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

@@ -5,7 +5,7 @@ import Link from 'next/link';
 
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
-import { useCurrentPagePath } from '~/stores/context';
+import { useCurrentPagePath } from '~/stores/page';
 import { useTimelineOptions } from '~/stores/renderer';
 import { useTimelineOptions } from '~/stores/renderer';
 
 
 import { RevisionLoader } from './Page/RevisionLoader';
 import { RevisionLoader } from './Page/RevisionLoader';

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

@@ -7,7 +7,7 @@ import {
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
   Dropdown, DropdownToggle, DropdownMenu, DropdownItem,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { useCurrentPagePath } from '~/stores/context';
+import { useCurrentPagePath } from '~/stores/page';
 
 
 import { RevisionDiff } from '../PageHistory/RevisionDiff';
 import { RevisionDiff } from '../PageHistory/RevisionDiff';
 
 

+ 2 - 1
packages/app/src/components/SavePageControls.tsx

@@ -11,9 +11,10 @@ import {
 import { CustomWindow } from '~/interfaces/global';
 import { CustomWindow } from '~/interfaces/global';
 import { IPageGrantData } from '~/interfaces/page';
 import { IPageGrantData } from '~/interfaces/page';
 import {
 import {
-  useCurrentPagePath, useIsEditable, useCurrentPageId, useIsAclEnabled,
+  useIsEditable, useCurrentPageId, useIsAclEnabled,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useIsEnabledUnsavedWarning } from '~/stores/editor';
 import { useIsEnabledUnsavedWarning } from '~/stores/editor';
+import { useCurrentPagePath } from '~/stores/page';
 import { useSelectedGrant } from '~/stores/ui';
 import { useSelectedGrant } from '~/stores/ui';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 

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

@@ -3,8 +3,9 @@ import React, { FC, memo } from 'react';
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
 import {
 import {
-  useCurrentPagePath, useCurrentPageId, useTargetAndAncestors, useIsGuestUser,
+  useCurrentPageId, useTargetAndAncestors, useIsGuestUser,
 } from '~/stores/context';
 } from '~/stores/context';
+import { useCurrentPagePath } from '~/stores/page';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 
 
 import ItemsTree from './PageTree/ItemsTree';
 import ItemsTree from './PageTree/ItemsTree';

+ 4 - 8
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -39,7 +39,7 @@ interface ItemProps {
   targetPathOrId?: Nullable<string>
   targetPathOrId?: Nullable<string>
   isOpen?: boolean
   isOpen?: boolean
   isEnabledAttachTitleHeader?: boolean
   isEnabledAttachTitleHeader?: boolean
-  onRenamed?(): void
+  onRenamed?(fromPath: string | undefined, toPath: string): void
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
   onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
   onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
 }
 }
@@ -191,7 +191,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       await mutateChildren();
       await mutateChildren();
 
 
       if (onRenamed != null) {
       if (onRenamed != null) {
-        onRenamed();
+        onRenamed(page.path, newPagePath);
       }
       }
 
 
       // force open
       // force open
@@ -286,7 +286,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       });
       });
 
 
       if (onRenamed != null) {
       if (onRenamed != null) {
-        onRenamed();
+        onRenamed(page.path, newPagePath);
       }
       }
 
 
       toastSuccess(t('renamed_pages', { path: page.path }));
       toastSuccess(t('renamed_pages', { path: page.path }));
@@ -380,11 +380,6 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const pathRecoveryMenuItemClickHandler = async(pageId: string): Promise<void> => {
   const pathRecoveryMenuItemClickHandler = async(pageId: string): Promise<void> => {
     try {
     try {
       await resumeRenameOperation(pageId);
       await resumeRenameOperation(pageId);
-
-      if (onRenamed != null) {
-        onRenamed();
-      }
-
       toastSuccess(t('page_operation.paths_recovered'));
       toastSuccess(t('page_operation.paths_recovered'));
     }
     }
     catch {
     catch {
@@ -425,6 +420,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   return (
   return (
     <div
     <div
       id={`pagetree-item-${page._id}`}
       id={`pagetree-item-${page._id}`}
+      data-testid="grw-pagetree-item-container"
       className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''}
       className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''}
     ${shouldHide ? 'd-none' : ''}`}
     ${shouldHide ? 'd-none' : ''}`}
     >
     >

+ 17 - 6
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -15,6 +15,7 @@ import { useIsEnabledAttachTitleHeader } from '~/stores/context';
 import {
 import {
   IPageForPageDuplicateModal, usePageDuplicateModal, usePageDeleteModal,
   IPageForPageDuplicateModal, usePageDuplicateModal, usePageDeleteModal,
 } from '~/stores/modal';
 } from '~/stores/modal';
+import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
 import {
 import {
   usePageTreeTermManager, useSWRxPageAncestorsChildren, useSWRxRootPage, useDescendantsPageListForCurrentPathTermManager,
   usePageTreeTermManager, useSWRxPageAncestorsChildren, useSWRxRootPage, useDescendantsPageListForCurrentPathTermManager,
 } from '~/stores/page-listing';
 } from '~/stores/page-listing';
@@ -102,6 +103,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 
 
   const { data: ancestorsChildrenResult, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
   const { data: ancestorsChildrenResult, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
   const { data: rootPageResult, error: error2 } = useSWRxRootPage();
   const { data: rootPageResult, error: error2 } = useSWRxRootPage();
+  const { data: currentPagePath } = useCurrentPagePath();
   const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { data: isEnabledAttachTitleHeader } = useIsEnabledAttachTitleHeader();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDuplicateModal } = usePageDuplicateModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
@@ -111,6 +113,7 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
   const { data: ptDescCountMap, update: updatePtDescCountMap } = usePageTreeDescCountMap();
   const { data: ptDescCountMap, update: updatePtDescCountMap } = usePageTreeDescCountMap();
 
 
   // for mutation
   // for mutation
+  const { mutate: mutateCurrentPage } = useSWRxCurrentPage();
   const { advance: advancePt } = usePageTreeTermManager();
   const { advance: advancePt } = usePageTreeTermManager();
   const { advance: advanceFts } = useFullTextSearchTermManager();
   const { advance: advanceFts } = useFullTextSearchTermManager();
   const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
   const { advance: advanceDpl } = useDescendantsPageListForCurrentPathTermManager();
@@ -142,13 +145,17 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
 
 
   }, [socket, ptDescCountMap, updatePtDescCountMap]);
   }, [socket, ptDescCountMap, updatePtDescCountMap]);
 
 
-  const onRenamed = () => {
+  const onRenamed = useCallback((fromPath: string | undefined, toPath: string) => {
     advancePt();
     advancePt();
     advanceFts();
     advanceFts();
     advanceDpl();
     advanceDpl();
-  };
 
 
-  const onClickDuplicateMenuItem = (pageToDuplicate: IPageForPageDuplicateModal) => {
+    if (currentPagePath === fromPath || currentPagePath === toPath) {
+      mutateCurrentPage();
+    }
+  }, [advanceDpl, advanceFts, advancePt, currentPagePath, mutateCurrentPage]);
+
+  const onClickDuplicateMenuItem = useCallback((pageToDuplicate: IPageForPageDuplicateModal) => {
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
       toastSuccess(t('duplicated_pages', { fromPath }));
       toastSuccess(t('duplicated_pages', { fromPath }));
@@ -159,9 +166,9 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
     };
     };
 
 
     openDuplicateModal(pageToDuplicate, { onDuplicated: duplicatedHandler });
     openDuplicateModal(pageToDuplicate, { onDuplicated: duplicatedHandler });
-  };
+  }, [advanceDpl, advanceFts, advancePt, openDuplicateModal, t]);
 
 
-  const onClickDeleteMenuItem = (pageToDelete: IPageToDeleteWithMeta) => {
+  const onClickDeleteMenuItem = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
     const onDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
     const onDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, isRecursively, isCompletely) => {
       if (typeof pathOrPathsToDelete !== 'string') {
       if (typeof pathOrPathsToDelete !== 'string') {
         return;
         return;
@@ -179,10 +186,14 @@ const ItemsTree = (props: ItemsTreeProps): JSX.Element => {
       advancePt();
       advancePt();
       advanceFts();
       advanceFts();
       advanceDpl();
       advanceDpl();
+
+      if (currentPagePath === pathOrPathsToDelete) {
+        mutateCurrentPage();
+      }
     };
     };
 
 
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
     openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
-  };
+  }, [advanceDpl, advanceFts, advancePt, currentPagePath, mutateCurrentPage, openDeleteModal, t]);
 
 
   // ***************************  Scroll on init ***************************
   // ***************************  Scroll on init ***************************
   const scrollOnInit = useCallback(() => {
   const scrollOnInit = useCallback(() => {

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

@@ -3,7 +3,7 @@ import React, { useCallback } from 'react';
 import { pagePathUtils } from '@growi/core';
 import { pagePathUtils } from '@growi/core';
 import ReactMarkdown from 'react-markdown';
 import ReactMarkdown from 'react-markdown';
 
 
-import { useCurrentPagePath } from '~/stores/context';
+import { useCurrentPagePath } from '~/stores/page';
 import { useTocOptions } from '~/stores/renderer';
 import { useTocOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 

+ 3 - 2
packages/app/src/components/User/UserDate.jsx

@@ -1,7 +1,8 @@
 import React from 'react';
 import React from 'react';
-import PropTypes from 'prop-types';
 
 
 import { format } from 'date-fns';
 import { format } from 'date-fns';
+import PropTypes from 'prop-types';
+
 
 
 /**
 /**
  * UserDate
  * UserDate
@@ -15,7 +16,7 @@ export default class UserDate extends React.Component {
     const dt = format(date, this.props.format);
     const dt = format(date, this.props.format);
 
 
     return (
     return (
-      <span className={this.props.className}>
+      <span className={this.props.className} data-hide-in-vrt>
         {dt}
         {dt}
       </span>
       </span>
     );
     );

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

@@ -56,7 +56,7 @@ import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 // import { serializeUserSecurely } from '../server/models/serializers/user-serializer';
 // import { serializeUserSecurely } from '../server/models/serializers/user-serializer';
 // import PageStatusAlert from '../client/js/components/PageStatusAlert';
 // import PageStatusAlert from '../client/js/components/PageStatusAlert';
 import {
 import {
-  useCurrentUser, useCurrentPagePath,
+  useCurrentUser,
   useIsLatestRevision,
   useIsLatestRevision,
   useIsForbidden, useIsNotFound, useIsSharedUser,
   useIsForbidden, useIsNotFound, useIsSharedUser,
   useIsEnabledStaleNotification, useIsIdenticalPath,
   useIsEnabledStaleNotification, useIsIdenticalPath,
@@ -239,7 +239,6 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
 
   useCurrentPageId(pageId ?? null);
   useCurrentPageId(pageId ?? null);
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
-  useCurrentPagePath(pagePath);
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
 
 
   useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data
   useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data

+ 2 - 2
packages/app/src/pages/admin/app.page.tsx

@@ -17,10 +17,10 @@ const AppSettingsPageContents = dynamic(() => import('~/components/Admin/App/App
 
 
 
 
 const AdminAppPage: NextPage<CommonProps> = (props) => {
 const AdminAppPage: NextPage<CommonProps> = (props) => {
-  const { t } = useTranslation('admin');
+  const { t } = useTranslation('commons');
   useIsMaintenanceMode(props.isMaintenanceMode);
   useIsMaintenanceMode(props.isMaintenanceMode);
 
 
-  const title = t('commons:headers.app_settings');
+  const title = t('headers.app_settings');
   const injectableContainers: Container<any>[] = [];
   const injectableContainers: Container<any>[] = [];
 
 
   if (isClient()) {
   if (isClient()) {

+ 9 - 0
packages/app/src/pages/admin/index.page.tsx

@@ -11,6 +11,7 @@ import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
 import PluginUtils from '~/server/plugins/plugin-utils';
 import PluginUtils from '~/server/plugins/plugin-utils';
 
 
+import { useGrowiCloudUri, useGrowiAppIdForGrowiCloud } from '../../stores/context';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../utils/admin-page-util';
 
 
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
@@ -22,10 +23,16 @@ type Props = CommonProps & {
   npmVersion: string,
   npmVersion: string,
   yarnVersion: string,
   yarnVersion: string,
   installedPlugins: any,
   installedPlugins: any,
+  growiCloudUri: string,
+  growiAppIdForGrowiCloud: number,
 };
 };
 
 
 
 
 const AdminHomePage: NextPage<Props> = (props) => {
 const AdminHomePage: NextPage<Props> = (props) => {
+
+  useGrowiCloudUri(props.growiCloudUri);
+  useGrowiAppIdForGrowiCloud(props.growiAppIdForGrowiCloud);
+
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
 
 
   const title = t('wiki_management_home_page');
   const title = t('wiki_management_home_page');
@@ -62,6 +69,8 @@ const injectServerConfigurations = async(context: GetServerSidePropsContext, pro
   props.npmVersion = crowi.runtimeVersions.versions.npm ? crowi.runtimeVersions.versions.npm.version.version : null;
   props.npmVersion = crowi.runtimeVersions.versions.npm ? crowi.runtimeVersions.versions.npm.version.version : null;
   props.yarnVersion = crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version.version : null;
   props.yarnVersion = crowi.runtimeVersions.versions.yarn ? crowi.runtimeVersions.versions.yarn.version.version : null;
   props.installedPlugins = pluginUtils.listPlugins();
   props.installedPlugins = pluginUtils.listPlugins();
+  props.growiCloudUri = await crowi.configManager.getConfig('crowi', 'app:growiCloudUri');
+  props.growiAppIdForGrowiCloud = await crowi.configManager.getConfig('crowi', 'app:growiAppIdForCloud');
 };
 };
 
 
 
 

+ 15 - 4
packages/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx

@@ -5,7 +5,9 @@ import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 import { useRouter } from 'next/router';
 
 
+import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useIsAclEnabled } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 import { retrieveServerSideProps } from '../../../utils/admin-page-util';
@@ -13,8 +15,11 @@ import { retrieveServerSideProps } from '../../../utils/admin-page-util';
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const AdminLayout = dynamic(() => import('~/components/Layout/AdminLayout'), { ssr: false });
 const UserGroupDetailPage = dynamic(() => import('~/components/Admin/UserGroupDetail/UserGroupDetailPage'), { ssr: false });
 const UserGroupDetailPage = dynamic(() => import('~/components/Admin/UserGroupDetail/UserGroupDetailPage'), { ssr: false });
 
 
+type Props = CommonProps & {
+  isAclEnabled: boolean
+}
 
 
-const AdminUserGroupDetailPage: NextPage<CommonProps> = (props) => {
+const AdminUserGroupDetailPage: NextPage<Props> = (props: Props) => {
   const { t } = useTranslation('admin');
   const { t } = useTranslation('admin');
   useIsMaintenanceMode(props.isMaintenanceMode);
   useIsMaintenanceMode(props.isMaintenanceMode);
   const router = useRouter();
   const router = useRouter();
@@ -23,9 +28,10 @@ const AdminUserGroupDetailPage: NextPage<CommonProps> = (props) => {
   const title = t('user_group_management.user_group_management');
   const title = t('user_group_management.user_group_management');
   const customTitle = useCustomTitle(props, title);
   const customTitle = useCustomTitle(props, title);
 
 
-
   const currentUserGroupId = Array.isArray(userGroupId) ? userGroupId[0] : userGroupId;
   const currentUserGroupId = Array.isArray(userGroupId) ? userGroupId[0] : userGroupId;
 
 
+  useIsAclEnabled(props.isAclEnabled);
+
   return (
   return (
     <AdminLayout title={customTitle} componentTitle={title} >
     <AdminLayout title={customTitle} componentTitle={title} >
       {
       {
@@ -36,10 +42,15 @@ const AdminUserGroupDetailPage: NextPage<CommonProps> = (props) => {
   );
   );
 };
 };
 
 
+const injectServerConfigurations = async(context: GetServerSidePropsContext, props: Props): Promise<void> => {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  props.isAclEnabled = req.crowi.aclService.isAclEnabled();
+};
+
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const props = await retrieveServerSideProps(context);
+  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+
   return props;
   return props;
 };
 };
 
 
-
 export default AdminUserGroupDetailPage;
 export default AdminUserGroupDetailPage;

+ 1 - 5
packages/app/src/pages/installer.page.tsx

@@ -10,8 +10,7 @@ import { NoLoginLayout } from '~/components/Layout/NoLoginLayout';
 
 
 import InstallerForm from '../components/InstallerForm';
 import InstallerForm from '../components/InstallerForm';
 import {
 import {
-  useCurrentPagePath, useCsrfToken,
-  useAppTitle, useSiteUrl, useConfidential,
+  useCsrfToken, useAppTitle, useSiteUrl, useConfidential,
 } from '../stores/context';
 } from '../stores/context';
 
 
 
 
@@ -40,9 +39,6 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
   useConfidential(props.confidential);
   useConfidential(props.confidential);
   useCsrfToken(props.csrfToken);
   useCsrfToken(props.csrfToken);
 
 
-  // page
-  useCurrentPagePath(props.currentPathname);
-
   const classNames: string[] = [];
   const classNames: string[] = [];
 
 
   return (
   return (

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

@@ -20,7 +20,7 @@ import { CrowiRequest } from '~/interfaces/crowi-request';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { IShareLinkHasId } from '~/interfaces/share-link';
 import { IShareLinkHasId } from '~/interfaces/share-link';
 import {
 import {
-  useCurrentUser, useCurrentPagePath, useCurrentPathname, useCurrentPageId, useRendererConfig, useIsSearchPage,
+  useCurrentUser, useCurrentPathname, useCurrentPageId, useRendererConfig, useIsSearchPage,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useDescendantsPageListModal } from '~/stores/modal';
@@ -50,7 +50,6 @@ const SharedPage: NextPage<Props> = (props: Props) => {
   useIsSearchPage(false);
   useIsSearchPage(false);
   useShareLinkId(props.shareLink?._id);
   useShareLinkId(props.shareLink?._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
-  useCurrentPagePath(props.shareLink?.relatedPage.path);
   useCurrentUser(props.currentUser);
   useCurrentUser(props.currentUser);
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
   useRendererConfig(props.rendererConfig);
   useRendererConfig(props.rendererConfig);

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

@@ -16,7 +16,7 @@ import {
 
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
 import {
-  useCurrentUser, useCurrentPageId, useCurrentPagePath, useCurrentPathname,
+  useCurrentUser, useCurrentPageId, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL, useIsGuestUser,
   useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL, useIsGuestUser,
 } from '../stores/context';
 } from '../stores/context';
@@ -52,7 +52,6 @@ const TrashPage: NextPage<CommonProps> = (props: Props) => {
   useIsSearchPage(false);
   useIsSearchPage(false);
   useCurrentPageId(null);
   useCurrentPageId(null);
   useCurrentPathname('/trash');
   useCurrentPathname('/trash');
-  useCurrentPagePath('/trash');
 
 
   // UserUISettings
   // UserUISettings
   usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);
   usePreferDrawerModeByUser(props.userUISettings?.preferDrawerModeByUser ?? props.sidebarConfig.isSidebarDrawerMode);

+ 18 - 14
packages/app/src/server/routes/login-passport.js

@@ -96,7 +96,7 @@ module.exports = function(crowi, app) {
    * @param {*} req
    * @param {*} req
    * @param {*} res
    * @param {*} res
    */
    */
-  const loginSuccessHandler = async(req, res, user, action) => {
+  const loginSuccessHandler = async(req, res, user, action, isExternalAccount = false) => {
 
 
     // update lastLoginAt
     // update lastLoginAt
     user.updateLastLoginAt(new Date(), (err, userData) => {
     user.updateLastLoginAt(new Date(), (err, userData) => {
@@ -106,12 +106,6 @@ module.exports = function(crowi, app) {
       }
       }
     });
     });
 
 
-    // check for redirection to '/invited'
-    const redirectTo = req.user.status === User.STATUS_INVITED ? '/invited' : req.session.redirectTo;
-
-    // remove session.redirectTo
-    delete req.session.redirectTo;
-
     const parameters = {
     const parameters = {
       ip:  req.ip,
       ip:  req.ip,
       endpoint: req.originalUrl,
       endpoint: req.originalUrl,
@@ -124,6 +118,16 @@ module.exports = function(crowi, app) {
 
 
     await crowi.activityService.createActivity(parameters);
     await crowi.activityService.createActivity(parameters);
 
 
+    if (isExternalAccount) {
+      return res.redirect('/');
+    }
+
+    // check for redirection to '/invited'
+    const redirectTo = req.user.status === User.STATUS_INVITED ? '/invited' : req.session.redirectTo;
+
+    // remove session.redirectTo
+    delete req.session.redirectTo;
+
     return res.apiv3({ redirectTo });
     return res.apiv3({ redirectTo });
   };
   };
 
 
@@ -253,7 +257,7 @@ module.exports = function(crowi, app) {
         return next(err);
         return next(err);
       }
       }
 
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_LDAP);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_LDAP, true);
     });
     });
   };
   };
 
 
@@ -418,7 +422,7 @@ module.exports = function(crowi, app) {
     req.logIn(user, async(err) => {
     req.logIn(user, async(err) => {
       if (err) { debug(err.message); return next() }
       if (err) { debug(err.message); return next() }
 
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_GOOGLE);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_GOOGLE, true);
     });
     });
   };
   };
 
 
@@ -461,7 +465,7 @@ module.exports = function(crowi, app) {
     req.logIn(user, async(err) => {
     req.logIn(user, async(err) => {
       if (err) { debug(err.message); return next() }
       if (err) { debug(err.message); return next() }
 
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_GITHUB);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_GITHUB, true);
     });
     });
   };
   };
 
 
@@ -504,7 +508,7 @@ module.exports = function(crowi, app) {
     req.logIn(user, async(err) => {
     req.logIn(user, async(err) => {
       if (err) { debug(err.message); return next() }
       if (err) { debug(err.message); return next() }
 
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_TWITTER);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_TWITTER, true);
     });
     });
   };
   };
 
 
@@ -553,7 +557,7 @@ module.exports = function(crowi, app) {
     req.logIn(user, async(err) => {
     req.logIn(user, async(err) => {
       if (err) { debug(err.message); return next() }
       if (err) { debug(err.message); return next() }
 
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_OIDC);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_OIDC, true);
     });
     });
   };
   };
 
 
@@ -616,7 +620,7 @@ module.exports = function(crowi, app) {
         return loginFailureHandler(req, res);
         return loginFailureHandler(req, res);
       }
       }
 
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_SAML);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_SAML, true);
     });
     });
   };
   };
 
 
@@ -659,7 +663,7 @@ module.exports = function(crowi, app) {
     await req.logIn(user, (err) => {
     await req.logIn(user, (err) => {
       if (err) { debug(err.message); return next() }
       if (err) { debug(err.message); return next() }
 
 
-      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_BASIC);
+      return loginSuccessHandler(req, res, user, SupportedAction.ACTION_USER_LOGIN_WITH_BASIC, true);
     });
     });
   };
   };
 
 

+ 57 - 67
packages/app/src/stores/context.tsx

@@ -13,6 +13,7 @@ import InterceptorManager from '~/services/interceptor-manager';
 
 
 import { TargetAndAncestors } from '../interfaces/page-listing-results';
 import { TargetAndAncestors } from '../interfaces/page-listing-results';
 
 
+import { useContextSWR } from './use-context-swr';
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
 
 
 
 
@@ -20,200 +21,204 @@ type Nullable<T> = T | null;
 
 
 
 
 export const useInterceptorManager = (): SWRResponse<InterceptorManager, Error> => {
 export const useInterceptorManager = (): SWRResponse<InterceptorManager, Error> => {
-  return useStaticSWR<InterceptorManager, Error>('interceptorManager', undefined, { fallbackData: new InterceptorManager() });
+  return useContextSWR<InterceptorManager, Error>('interceptorManager', undefined, { fallbackData: new InterceptorManager() });
 };
 };
 
 
 export const useCsrfToken = (initialData?: string): SWRResponse<string, Error> => {
 export const useCsrfToken = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR<string, Error>('csrfToken', initialData);
+  return useContextSWR<string, Error>('csrfToken', initialData);
 };
 };
 
 
 export const useAppTitle = (initialData?: string): SWRResponse<string, Error> => {
 export const useAppTitle = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR('appTitle', initialData);
+  return useContextSWR('appTitle', initialData);
 };
 };
 
 
 export const useSiteUrl = (initialData?: string): SWRResponse<string, Error> => {
 export const useSiteUrl = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR<string, Error>('siteUrl', initialData);
+  return useContextSWR<string, Error>('siteUrl', initialData);
 };
 };
 
 
 export const useConfidential = (initialData?: string): SWRResponse<string, Error> => {
 export const useConfidential = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR('confidential', initialData);
+  return useContextSWR('confidential', initialData);
 };
 };
 
 
 export const useGrowiTheme = (initialData?: GrowiThemes): SWRResponse<GrowiThemes, Error> => {
 export const useGrowiTheme = (initialData?: GrowiThemes): SWRResponse<GrowiThemes, Error> => {
-  return useStaticSWR('theme', initialData);
+  return useContextSWR('theme', initialData);
 };
 };
 
 
 export const useCurrentUser = (initialData?: Nullable<IUser>): SWRResponse<Nullable<IUser>, Error> => {
 export const useCurrentUser = (initialData?: Nullable<IUser>): SWRResponse<Nullable<IUser>, Error> => {
-  return useStaticSWR<Nullable<IUser>, Error>('currentUser', initialData);
+  return useContextSWR<Nullable<IUser>, Error>('currentUser', initialData);
 };
 };
 
 
 export const useRevisionId = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
 export const useRevisionId = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('revisionId', initialData);
-};
-
-export const useCurrentPagePath = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('currentPagePath', initialData);
+  return useContextSWR<Nullable<any>, Error>('revisionId', initialData);
 };
 };
 
 
 export const useCurrentPathname = (initialData?: string): SWRResponse<string, Error> => {
 export const useCurrentPathname = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR('currentPathname', initialData);
+  return useContextSWR('currentPathname', initialData);
 };
 };
 
 
 export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
 export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('currentPageId', initialData);
+  return useContextSWR<Nullable<string>, Error>('currentPageId', initialData);
 };
 };
 
 
 export const useIsIdenticalPath = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsIdenticalPath = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isIdenticalPath', initialData, { fallbackData: false });
+  return useContextSWR<boolean, Error>('isIdenticalPath', initialData, { fallbackData: false });
 };
 };
 
 
 export const useIsForbidden = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsForbidden = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isForbidden', initialData, { fallbackData: false });
+  return useContextSWR<boolean, Error>('isForbidden', initialData, { fallbackData: false });
 };
 };
 
 
 export const useIsNotFound = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsNotFound = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isNotFound', initialData, { fallbackData: false });
+  return useContextSWR<boolean, Error>('isNotFound', initialData, { fallbackData: false });
 };
 };
 
 
 export const useTemplateTagData = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
 export const useTemplateTagData = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('templateTagData', initialData);
+  return useContextSWR<Nullable<string>, Error>('templateTagData', initialData);
 };
 };
 
 
 export const useIsSharedUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsSharedUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isSharedUser', initialData);
+  return useContextSWR<boolean, Error>('isSharedUser', initialData);
 };
 };
 
 
 export const useShareLinkId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
 export const useShareLinkId = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('shareLinkId', initialData);
+  return useContextSWR<Nullable<string>, Error>('shareLinkId', initialData);
 };
 };
 
 
 export const useDisableLinkSharing = (initialData?: Nullable<boolean>): SWRResponse<Nullable<boolean>, Error> => {
 export const useDisableLinkSharing = (initialData?: Nullable<boolean>): SWRResponse<Nullable<boolean>, Error> => {
-  return useStaticSWR<Nullable<boolean>, Error>('disableLinkSharing', initialData);
+  return useContextSWR<Nullable<boolean>, Error>('disableLinkSharing', initialData);
 };
 };
 
 
 export const useRegistrationWhiteList = (initialData?: Nullable<string[]>): SWRResponse<Nullable<string[]>, Error> => {
 export const useRegistrationWhiteList = (initialData?: Nullable<string[]>): SWRResponse<Nullable<string[]>, Error> => {
-  return useStaticSWR<Nullable<string[]>, Error>('registrationWhiteList', initialData);
+  return useContextSWR<Nullable<string[]>, Error>('registrationWhiteList', initialData);
 };
 };
 
 
 export const useDrawioUri = (initialData?: string): SWRResponse<string, Error> => {
 export const useDrawioUri = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR('drawioUri', initialData, { fallbackData: 'https://embed.diagrams.net/' });
+  return useContextSWR('drawioUri', initialData, { fallbackData: 'https://embed.diagrams.net/' });
 };
 };
 
 
 export const useHackmdUri = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
 export const useHackmdUri = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useStaticSWR<Nullable<string>, Error>('hackmdUri', initialData);
+  return useContextSWR<Nullable<string>, Error>('hackmdUri', initialData);
 };
 };
 
 
 export const useIsSearchPage = (initialData?: Nullable<any>) : SWRResponse<Nullable<any>, Error> => {
 export const useIsSearchPage = (initialData?: Nullable<any>) : SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('isSearchPage', initialData);
+  return useContextSWR<Nullable<any>, Error>('isSearchPage', initialData);
 };
 };
 
 
 export const useTargetAndAncestors = (initialData?: TargetAndAncestors): SWRResponse<TargetAndAncestors, Error> => {
 export const useTargetAndAncestors = (initialData?: TargetAndAncestors): SWRResponse<TargetAndAncestors, Error> => {
-  return useStaticSWR<TargetAndAncestors, Error>('targetAndAncestors', initialData);
+  return useContextSWR<TargetAndAncestors, Error>('targetAndAncestors', initialData);
 };
 };
 
 
 export const useIsAclEnabled = (initialData?: boolean) : SWRResponse<boolean, Error> => {
 export const useIsAclEnabled = (initialData?: boolean) : SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isAclEnabled', initialData);
+  return useContextSWR<boolean, Error>('isAclEnabled', initialData);
 };
 };
 
 
 export const useIsSearchServiceConfigured = (initialData?: boolean) : SWRResponse<boolean, Error> => {
 export const useIsSearchServiceConfigured = (initialData?: boolean) : SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isSearchServiceConfigured', initialData);
+  return useContextSWR<boolean, Error>('isSearchServiceConfigured', initialData);
 };
 };
 
 
 export const useIsSearchServiceReachable = (initialData?: boolean) : SWRResponse<boolean, Error> => {
 export const useIsSearchServiceReachable = (initialData?: boolean) : SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isSearchServiceReachable', initialData);
+  return useContextSWR<boolean, Error>('isSearchServiceReachable', initialData);
 };
 };
 
 
 export const useIsMailerSetup = (initialData?: boolean): SWRResponse<boolean, any> => {
 export const useIsMailerSetup = (initialData?: boolean): SWRResponse<boolean, any> => {
-  return useStaticSWR('isMailerSetup', initialData);
+  return useContextSWR('isMailerSetup', initialData);
 };
 };
 
 
 export const useIsSearchScopeChildrenAsDefault = (initialData?: boolean) : SWRResponse<boolean, Error> => {
 export const useIsSearchScopeChildrenAsDefault = (initialData?: boolean) : SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isSearchScopeChildrenAsDefault', initialData);
+  return useContextSWR<boolean, Error>('isSearchScopeChildrenAsDefault', initialData);
 };
 };
 
 
 export const useIsSlackConfigured = (initialData?: boolean) : SWRResponse<boolean, Error> => {
 export const useIsSlackConfigured = (initialData?: boolean) : SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isSlackConfigured', initialData);
+  return useContextSWR<boolean, Error>('isSlackConfigured', initialData);
 };
 };
 
 
 export const useIsEnabledAttachTitleHeader = (initialData?: boolean) : SWRResponse<boolean, Error> => {
 export const useIsEnabledAttachTitleHeader = (initialData?: boolean) : SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isEnabledAttachTitleHeader', initialData);
+  return useContextSWR<boolean, Error>('isEnabledAttachTitleHeader', initialData);
 };
 };
 
 
 export const useIsIndentSizeForced = (initialData?: boolean) : SWRResponse<boolean, Error> => {
 export const useIsIndentSizeForced = (initialData?: boolean) : SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('isIndentSizeForced', initialData, { fallbackData: false });
+  return useContextSWR<boolean, Error>('isIndentSizeForced', initialData, { fallbackData: false });
 };
 };
 
 
 export const useDefaultIndentSize = (initialData?: number) : SWRResponse<number, Error> => {
 export const useDefaultIndentSize = (initialData?: number) : SWRResponse<number, Error> => {
-  return useStaticSWR<number, Error>('defaultIndentSize', initialData, { fallbackData: 4 });
+  return useContextSWR<number, Error>('defaultIndentSize', initialData, { fallbackData: 4 });
 };
 };
 
 
 export const useAuditLogEnabled = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useAuditLogEnabled = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR<boolean, Error>('auditLogEnabled', initialData, { fallbackData: false });
+  return useContextSWR<boolean, Error>('auditLogEnabled', initialData, { fallbackData: false });
 };
 };
 
 
 // TODO: initialize in [[..path]].page.tsx?
 // TODO: initialize in [[..path]].page.tsx?
 export const useActivityExpirationSeconds = (initialData?: number) : SWRResponse<number, Error> => {
 export const useActivityExpirationSeconds = (initialData?: number) : SWRResponse<number, Error> => {
-  return useStaticSWR<number, Error>('activityExpirationSeconds', initialData);
+  return useContextSWR<number, Error>('activityExpirationSeconds', initialData);
 };
 };
 
 
 export const useAuditLogAvailableActions = (initialData?: Array<SupportedActionType>) : SWRResponse<Array<SupportedActionType>, Error> => {
 export const useAuditLogAvailableActions = (initialData?: Array<SupportedActionType>) : SWRResponse<Array<SupportedActionType>, Error> => {
-  return useStaticSWR<Array<SupportedActionType>, Error>('auditLogAvailableActions', initialData);
+  return useContextSWR<Array<SupportedActionType>, Error>('auditLogAvailableActions', initialData);
 };
 };
 
 
 export const useGrowiVersion = (initialData?: string): SWRResponse<string, any> => {
 export const useGrowiVersion = (initialData?: string): SWRResponse<string, any> => {
-  return useStaticSWR('growiVersion', initialData);
+  return useContextSWR('growiVersion', initialData);
 };
 };
 
 
 export const useIsEnabledStaleNotification = (initialData?: boolean): SWRResponse<boolean, any> => {
 export const useIsEnabledStaleNotification = (initialData?: boolean): SWRResponse<boolean, any> => {
-  return useStaticSWR('isEnabledStaleNotification', initialData);
+  return useContextSWR('isEnabledStaleNotification', initialData);
 };
 };
 
 
 export const useIsLatestRevision = (initialData?: boolean): SWRResponse<boolean, any> => {
 export const useIsLatestRevision = (initialData?: boolean): SWRResponse<boolean, any> => {
-  return useStaticSWR('isLatestRevision', initialData);
+  return useContextSWR('isLatestRevision', initialData);
 };
 };
 
 
 export const useEditorConfig = (initialData?: EditorConfig): SWRResponse<EditorConfig, Error> => {
 export const useEditorConfig = (initialData?: EditorConfig): SWRResponse<EditorConfig, Error> => {
-  return useStaticSWR<EditorConfig, Error>('editorConfig', initialData);
+  return useContextSWR<EditorConfig, Error>('editorConfig', initialData);
 };
 };
 
 
 export const useRendererConfig = (initialData?: RendererConfig): SWRResponse<RendererConfig, any> => {
 export const useRendererConfig = (initialData?: RendererConfig): SWRResponse<RendererConfig, any> => {
-  return useStaticSWR('growiRendererConfig', initialData);
+  return useContextSWR('growiRendererConfig', initialData);
 };
 };
 
 
 export const useIsAllReplyShown = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsAllReplyShown = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR('isAllReplyShown', initialData);
+  return useContextSWR('isAllReplyShown', initialData);
 };
 };
 
 
 export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsBlinkedHeaderAtBoot = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR('isBlinkedAtBoot', initialData, { fallbackData: false });
+  return useContextSWR('isBlinkedAtBoot', initialData, { fallbackData: false });
 };
 };
 
 
 export const useEditingMarkdown = (initialData?: string): SWRResponse<string, Error> => {
 export const useEditingMarkdown = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR('currentMarkdown', initialData);
+  return useContextSWR('currentMarkdown', initialData);
 };
 };
 
 
 export const useIsUploadableImage = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsUploadableImage = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR('isUploadableImage', initialData);
+  return useContextSWR('isUploadableImage', initialData);
 };
 };
 
 
 export const useIsUploadableFile = (initialData?: boolean): SWRResponse<boolean, Error> => {
 export const useIsUploadableFile = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useStaticSWR('isUploadableFile', initialData);
+  return useContextSWR('isUploadableFile', initialData);
 };
 };
 
 
 export const useShowPageLimitationL = (initialData?: number): SWRResponse<number, Error> => {
 export const useShowPageLimitationL = (initialData?: number): SWRResponse<number, Error> => {
-  return useStaticSWR('showPageLimitationL', initialData);
+  return useContextSWR('showPageLimitationL', initialData);
 };
 };
 
 
 export const useShowPageLimitationXL = (initialData?: number): SWRResponse<number, Error> => {
 export const useShowPageLimitationXL = (initialData?: number): SWRResponse<number, Error> => {
-  return useStaticSWR('showPageLimitationXL', initialData);
+  return useContextSWR('showPageLimitationXL', initialData);
 };
 };
 
 
 export const useCustomizeTitle = (initialData?: string): SWRResponse<string, Error> => {
 export const useCustomizeTitle = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR('CustomizeTitle', initialData);
+  return useContextSWR('CustomizeTitle', initialData);
 };
 };
 
 
 export const useCustomizedLogoSrc = (initialData?: string): SWRResponse<string, Error> => {
 export const useCustomizedLogoSrc = (initialData?: string): SWRResponse<string, Error> => {
-  return useStaticSWR('customizedLogoSrc', initialData);
+  return useContextSWR('customizedLogoSrc', initialData);
+};
+
+export const useGrowiCloudUri = (initialData?: string): SWRResponse<string, Error> => {
+  return useStaticSWR('growiCloudUri', initialData);
+};
+
+export const useGrowiAppIdForGrowiCloud = (initialData?: number): SWRResponse<number, Error> => {
+  return useStaticSWR('growiAppIdForGrowiCloud', initialData);
 };
 };
 
 
 /** **********************************************************
 /** **********************************************************
@@ -242,18 +247,3 @@ export const useIsEditable = (): SWRResponse<boolean, Error> => {
     },
     },
   );
   );
 };
 };
-
-export const useCurrentPageTocNode = (): SWRResponse<HtmlElementNode, any> => {
-  const { data: currentPagePath } = useCurrentPagePath();
-
-  return useStaticSWR(['currentPageTocNode', currentPagePath]);
-};
-
-export const useIsTrashPage = (): SWRResponse<boolean, Error> => {
-  const { data: pagePath } = useCurrentPagePath();
-
-  return useSWRImmutable(
-    pagePath == null ? null : ['isTrashPage', pagePath],
-    (key: Key, pagePath: string) => pagePathUtils.isTrashPage(pagePath),
-  );
-};

+ 1 - 1
packages/app/src/stores/page-listing.tsx

@@ -13,7 +13,7 @@ import {
   AncestorsChildrenResult, ChildrenResult, V5MigrationStatus, RootPageResult,
   AncestorsChildrenResult, ChildrenResult, V5MigrationStatus, RootPageResult,
 } from '../interfaces/page-listing-results';
 } from '../interfaces/page-listing-results';
 
 
-import { useCurrentPagePath } from './context';
+import { useCurrentPagePath } from './page';
 import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
 
 export const useSWRxPagesByPath = (path?: Nullable<string>): SWRResponse<IPageHasId[], Error> => {
 export const useSWRxPagesByPath = (path?: Nullable<string>): SWRResponse<IPageHasId[], Error> => {

+ 1 - 1
packages/app/src/stores/page-redirect.tsx

@@ -3,7 +3,7 @@ import { SWRResponse } from 'swr';
 
 
 import { apiPost } from '~/client/util/apiv1-client';
 import { apiPost } from '~/client/util/apiv1-client';
 
 
-import { useCurrentPagePath } from './context';
+import { useCurrentPagePath } from './page';
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
 
 
 type RedirectFromUtil = {
 type RedirectFromUtil = {

+ 52 - 16
packages/app/src/stores/page.tsx

@@ -1,5 +1,8 @@
-import { IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable } from '@growi/core';
-import useSWR, { SWRResponse } from 'swr';
+import type {
+  IPageInfoForEntity, IPagePopulatedToShowRevision, Nullable,
+} from '@growi/core';
+import { pagePathUtils } from '@growi/core';
+import useSWR, { Key, SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { apiGet } from '~/client/util/apiv1-client';
@@ -12,10 +15,17 @@ import { IRevisionsForPagination } from '~/interfaces/revision';
 
 
 import { IPageTagsInfo } from '../interfaces/tag';
 import { IPageTagsInfo } from '../interfaces/tag';
 
 
-import { useCurrentPageId } from './context';
+import { useCurrentPageId, useCurrentPathname } from './context';
 
 
 
 
-export const useSWRxPage = (pageId?: string|null, shareLinkId?: string): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
+const { isPermalink: _isPermalink } = pagePathUtils;
+
+
+export const useSWRxPage = (
+    pageId?: string|null,
+    shareLinkId?: string,
+    initialData?: IPagePopulatedToShowRevision|null,
+): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
   return useSWR<IPagePopulatedToShowRevision|null, Error>(
   return useSWR<IPagePopulatedToShowRevision|null, Error>(
     pageId != null ? ['/page', pageId, shareLinkId] : null,
     pageId != null ? ['/page', pageId, shareLinkId] : null,
     (endpoint, pageId, shareLinkId) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId })
     (endpoint, pageId, shareLinkId) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId })
@@ -29,6 +39,7 @@ export const useSWRxPage = (pageId?: string|null, shareLinkId?: string): SWRResp
         }
         }
         throw Error('failed to get page');
         throw Error('failed to get page');
       }),
       }),
+    { fallbackData: initialData },
   );
   );
 };
 };
 
 
@@ -44,13 +55,7 @@ export const useSWRxCurrentPage = (
 ): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
 ): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
 
 
-  const swrResult = useSWRxPage(currentPageId, shareLinkId);
-
-  // use mutate because fallbackData does not work
-  // see: https://github.com/weseek/growi/commit/5038473e8d6028c9c91310e374a7b5f48b921a15
-  if (initialData !== undefined) {
-    swrResult.mutate(initialData);
-  }
+  const swrResult = useSWRxPage(currentPageId, shareLinkId, initialData);
 
 
   return swrResult;
   return swrResult;
 };
 };
@@ -90,11 +95,6 @@ export const useSWRxPageInfo = (
     { fallbackData: initialData },
     { fallbackData: initialData },
   );
   );
 
 
-  // use mutate because fallbackData does not work
-  if (initialData != null) {
-    swrResult.mutate(initialData);
-  }
-
   return swrResult;
   return swrResult;
 };
 };
 
 
@@ -140,3 +140,39 @@ export const useSWRxApplicableGrant = (
     (endpoint, pageId) => apiv3Get(endpoint, { pageId }).then(response => response.data),
     (endpoint, pageId) => apiv3Get(endpoint, { pageId }).then(response => response.data),
   );
   );
 };
 };
+
+
+/** **********************************************************
+ *                     Computed states
+ *********************************************************** */
+
+export const useCurrentPagePath = (): SWRResponse<string | undefined, Error> => {
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: currentPathname } = useCurrentPathname();
+
+  return useSWRImmutable(
+    ['currentPagePath', currentPage?.path, currentPathname],
+    (key: Key, pagePath: string|undefined, pathname: string|undefined) => {
+      if (currentPage?.path != null) {
+        return currentPage.path;
+      }
+      if (pathname != null && !_isPermalink(pathname)) {
+        return pathname;
+      }
+      return undefined;
+    },
+    // TODO: set fallbackData
+    // { fallbackData:  }
+  );
+};
+
+export const useIsTrashPage = (): SWRResponse<boolean, Error> => {
+  const { data: pagePath } = useCurrentPagePath();
+
+  return useSWRImmutable(
+    pagePath == null ? null : ['isTrashPage', pagePath],
+    (key: Key, pagePath: string) => pagePathUtils.isTrashPage(pagePath),
+    // TODO: set fallbackData
+    // { fallbackData:  }
+  );
+};

+ 3 - 1
packages/app/src/stores/renderer.tsx

@@ -11,8 +11,10 @@ import {
 
 
 
 
 import {
 import {
-  useCurrentPagePath, useCurrentPageTocNode, useRendererConfig,
+  useRendererConfig,
 } from './context';
 } from './context';
+import { useCurrentPagePath } from './page';
+import { useCurrentPageTocNode } from './ui';
 
 
 interface ReactMarkdownOptionsGenerator {
 interface ReactMarkdownOptionsGenerator {
   (config: RendererConfig): RendererOptions
   (config: RendererConfig): RendererOptions

+ 9 - 2
packages/app/src/stores/ui.tsx

@@ -5,6 +5,7 @@ import {
 } from '@growi/core';
 } from '@growi/core';
 import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
 import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
 import { Breakpoint, addBreakpointListener, cleanupBreakpointListener } from '@growi/ui';
 import { Breakpoint, addBreakpointListener, cleanupBreakpointListener } from '@growi/ui';
+import { HtmlElementNode } from 'rehype-toc';
 import SimpleBar from 'simplebar-react';
 import SimpleBar from 'simplebar-react';
 import {
 import {
   useSWRConfig, SWRResponse, Key, Fetcher,
   useSWRConfig, SWRResponse, Key, Fetcher,
@@ -21,10 +22,11 @@ import { UpdateDescCountData } from '~/interfaces/websocket';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import {
 import {
-  useCurrentPageId, useCurrentPagePath, useIsEditable, useIsTrashPage, useIsGuestUser,
+  useCurrentPageId, useIsEditable, useIsGuestUser,
   useIsSharedUser, useIsIdenticalPath, useCurrentUser, useIsNotFound, useShareLinkId,
   useIsSharedUser, useIsIdenticalPath, useCurrentUser, useIsNotFound, useShareLinkId,
 } from './context';
 } from './context';
 import { localStorageMiddleware } from './middlewares/sync-to-storage';
 import { localStorageMiddleware } from './middlewares/sync-to-storage';
+import { useCurrentPagePath, useIsTrashPage } from './page';
 import { useStaticSWR } from './use-static-swr';
 import { useStaticSWR } from './use-static-swr';
 
 
 const { isTrashTopPage, isUsersTopPage } = pagePathUtils;
 const { isTrashTopPage, isUsersTopPage } = pagePathUtils;
@@ -45,13 +47,18 @@ export type EditorMode = typeof EditorMode[keyof typeof EditorMode];
 
 
 
 
 /** **********************************************************
 /** **********************************************************
- *                     Storing RefObjects
+ *                     Storing objects to ref
  *********************************************************** */
  *********************************************************** */
 
 
 export const useSidebarScrollerRef = (initialData?: RefObject<SimpleBar>): SWRResponse<RefObject<SimpleBar>, Error> => {
 export const useSidebarScrollerRef = (initialData?: RefObject<SimpleBar>): SWRResponse<RefObject<SimpleBar>, Error> => {
   return useStaticSWR<RefObject<SimpleBar>, Error>('sidebarScrollerRef', initialData);
   return useStaticSWR<RefObject<SimpleBar>, Error>('sidebarScrollerRef', initialData);
 };
 };
 
 
+export const useCurrentPageTocNode = (): SWRResponse<HtmlElementNode, any> => {
+  const { data: currentPagePath } = useCurrentPagePath();
+
+  return useStaticSWR(['currentPageTocNode', currentPagePath]);
+};
 
 
 /** **********************************************************
 /** **********************************************************
  *                          SWR Hooks
  *                          SWR Hooks

+ 24 - 0
packages/app/src/stores/use-context-swr.tsx

@@ -0,0 +1,24 @@
+import {
+  Key, SWRConfiguration, SWRResponse,
+} from 'swr';
+
+import { useStaticSWR } from './use-static-swr';
+
+export function useContextSWR<Data, Error>(key: Key): SWRResponse<Data, Error>;
+export function useContextSWR<Data, Error>(key: Key, data: Data | undefined): SWRResponse<Data, Error>;
+export function useContextSWR<Data, Error>(key: Key, data: Data | undefined,
+  configuration: SWRConfiguration<Data, Error> | undefined): SWRResponse<Data, Error>;
+
+export function useContextSWR<Data, Error>(
+    ...args: readonly [Key]
+    | readonly [Key, Data | undefined]
+    | readonly [Key, Data | undefined, SWRConfiguration<Data, Error> | undefined]
+): SWRResponse<Data, Error> {
+  const [key, data, configuration] = args;
+
+  const swrResponse = useStaticSWR<Data, Error>(key, data, configuration);
+
+  const result = Object.assign(swrResponse, { mutate: () => { throw Error('mutate can not be used in context') } });
+
+  return result;
+}

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

@@ -26,6 +26,7 @@ context('Access to page', () => {
     // https://stackoverflow.com/questions/5041494/selecting-and-manipulating-css-pseudo-elements-such-as-before-and-after-usin/21709814#21709814
     // https://stackoverflow.com/questions/5041494/selecting-and-manipulating-css-pseudo-elements-such-as-before-and-after-usin/21709814#21709814
     cy.get('#mdcont-headers').invoke('removeClass', 'blink');
     cy.get('#mdcont-headers').invoke('removeClass', 'blink');
 
 
+    cy.get('.grw-skelton').should('not.exist');
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
     cy.screenshot(`${ssPrefix}-sandbox-headers`);
   });
   });
 
 
@@ -45,6 +46,14 @@ context('Access to page', () => {
 
 
   it('/user/admin is successfully loaded', () => {
   it('/user/admin is successfully loaded', () => {
     cy.visit('/user/admin', {  });
     cy.visit('/user/admin', {  });
+
+    cy.get('.grw-skelton').should('not.exist');
+    // for check download toc data
+    cy.get('.toc-link').should('be.visible');
+
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(2000); // wait for calcViewHeight and rendering
+
     cy.screenshot(`${ssPrefix}-user-admin`);
     cy.screenshot(`${ssPrefix}-user-admin`);
   });
   });
 
 

+ 6 - 3
packages/app/test/cypress/integration/20-basic-features/click-page-icons.spec.ts

@@ -77,11 +77,14 @@ context('Click page icons button', () => {
 
 
   it('Successfully display list of "seen by user"', () => {
   it('Successfully display list of "seen by user"', () => {
     cy.visit('/Sandbox');
     cy.visit('/Sandbox');
+    cy.get('.grw-skelton').should('not.exist');
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(2000); // wait for get method
     cy.get('#grw-subnav-container').within(() => {
     cy.get('#grw-subnav-container').within(() => {
-      cy.get('div.grw-seen-user-info > button#btn-seen-user').click({force: true});
+      cy.get('div.grw-seen-user-info').find('button#btn-seen-user').click({force: true});
     });
     });
-    // TODO:
-    // cy.get('div.user-list-popover').should('be.visible');
+
+    cy.get('.user-list-popover').should('be.visible')
 
 
     cy.get('#grw-subnav-container').within(() => {
     cy.get('#grw-subnav-container').within(() => {
       cy.screenshot(`${ssPrefix}11-seen-user-list`);
       cy.screenshot(`${ssPrefix}11-seen-user-list`);

+ 4 - 4
packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts

@@ -176,7 +176,7 @@ context('Page Accessories Modal', () => {
   });
   });
 
 
   it('Page History is shown successfully', () => {
   it('Page History is shown successfully', () => {
-     cy.visit('/Sandbox/Bootstrap4', {  });
+     cy.visit('/Sandbox/Bootstrap4');
      cy.get('#grw-subnav-container').within(() => {
      cy.get('#grw-subnav-container').within(() => {
       cy.getByTestid('open-page-item-control-btn').within(() => {
       cy.getByTestid('open-page-item-control-btn').within(() => {
         cy.get('button.btn-page-item-control').click({force: true});
         cy.get('button.btn-page-item-control').click({force: true});
@@ -313,9 +313,9 @@ context('Tag Oprations', () =>{
           cy.wrap($row).within(() => {
           cy.wrap($row).within(() => {
             cy.getByTestid('open-page-item-control-btn').first().click();
             cy.getByTestid('open-page-item-control-btn').first().click();
             cy.getByTestid('page-item-control-menu').should('have.class', 'show').first().within(() => {
             cy.getByTestid('page-item-control-menu').should('have.class', 'show').first().within(() => {
-            // eslint-disable-next-line cypress/no-unnecessary-waiting
-            cy.wait(300);
-            cy.screenshot(`${ssPrefix}2-open-page-item-control-menu`);
+              // empty sentence in page list empty: https://github.com/weseek/growi/pull/6880
+              cy.getByTestid('revision-short-body-in-page-list-item-L').invoke('text', '');
+              cy.screenshot(`${ssPrefix}2-open-page-item-control-menu`);
             })
             })
           });
           });
         }
         }

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

@@ -2,7 +2,8 @@ context('Access to page by guest', () => {
   const ssPrefix = 'access-to-page-by-guest-';
   const ssPrefix = 'access-to-page-by-guest-';
 
 
   it('/Sandbox is successfully loaded', () => {
   it('/Sandbox is successfully loaded', () => {
-    cy.visit('/Sandbox', {  });
+    cy.visit('/Sandbox');
+    cy.getByTestid('grw-pagetree-item-container').should('be.visible');
     cy.collapseSidebar(true, true);
     cy.collapseSidebar(true, true);
     cy.screenshot(`${ssPrefix}-sandbox`);
     cy.screenshot(`${ssPrefix}-sandbox`);
   });
   });

+ 12 - 6
packages/app/test/cypress/integration/50-sidebar/access-to-side-bar.spec.ts

@@ -29,8 +29,7 @@ context('Access to sidebar', () => {
     cy.getByTestid('grw-recent-changes').should('be.visible');
     cy.getByTestid('grw-recent-changes').should('be.visible');
     cy.get('.list-group-item').should('be.visible');
     cy.get('.list-group-item').should('be.visible');
 
 
-    // Avoid blackout misalignment
-    cy.scrollTo('center');
+    cy.scrollTo('top');
     cy.screenshot(`${ssPrefix}recent-changes-1-page-list`);
     cy.screenshot(`${ssPrefix}recent-changes-1-page-list`);
 
 
     cy.get('#grw-sidebar-contents-wrapper').within(() => {
     cy.get('#grw-sidebar-contents-wrapper').within(() => {
@@ -38,8 +37,7 @@ context('Access to sidebar', () => {
       cy.get('.list-group-item').should('be.visible');
       cy.get('.list-group-item').should('be.visible');
     });
     });
 
 
-    // Avoid blackout misalignment
-    cy.scrollTo('center');
+    cy.scrollTo('top');
     cy.screenshot(`${ssPrefix}recent-changes-2-switch-sidebar-size`);
     cy.screenshot(`${ssPrefix}recent-changes-2-switch-sidebar-size`);
   });
   });
 
 
@@ -81,8 +79,11 @@ context('Access to sidebar', () => {
         cy.getByTestid('grw-navigation-resize-button').click({force: true});
         cy.getByTestid('grw-navigation-resize-button').click({force: true});
       }
       }
     });
     });
-    cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}page-tree-1-access-to-page-tree`);
-    cy.get('.grw-pagetree-triangle-btn').eq(0).click();
+
+    cy.getByTestid('grw-contextual-navigation-sub').should('be.visible')
+    cy.get('.grw-pagetree-item-children').eq(0).should('be.visible');
+    cy.screenshot(`${ssPrefix}page-tree-1-access-to-page-tree`);
+
     cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}page-tree-2-hide-page-tree-item`);
     cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}page-tree-2-hide-page-tree-item`);
     cy.get('.grw-pagetree-triangle-btn').eq(0).click();
     cy.get('.grw-pagetree-triangle-btn').eq(0).click();
 
 
@@ -180,6 +181,11 @@ context('Access to sidebar', () => {
     cy.get('.grw-sidebar-nav-secondary-container').within(() => {
     cy.get('.grw-sidebar-nav-secondary-container').within(() => {
       cy.get('a[href*="/trash"]').click();
       cy.get('a[href*="/trash"]').click();
     });
     });
+
+    cy.get('.grw-page-path-hierarchical-link').should('be.visible');
+
+    cy.get('.grw-custom-nav-tab').should('be.visible');
+
     cy.screenshot(`${ssPrefix}access-to-trash-page`);
     cy.screenshot(`${ssPrefix}access-to-trash-page`);
   });
   });
 });
 });

+ 5 - 1
packages/app/test/cypress/integration/60-home/home.spec.ts

@@ -15,10 +15,14 @@ context('Access Home', () => {
     cy.getByTestid('grw-personal-dropdown').click();
     cy.getByTestid('grw-personal-dropdown').click();
     cy.getByTestid('grw-personal-dropdown').find('.dropdown-menu .btn-group > .btn-outline-secondary:eq(0)').click();
     cy.getByTestid('grw-personal-dropdown').find('.dropdown-menu .btn-group > .btn-outline-secondary:eq(0)').click();
 
 
-    cy.get('.grw-users-info').should('be.visible');
+    cy.get('.grw-skelton').should('not.exist');
     // for check download toc data
     // for check download toc data
     cy.get('.toc-link').should('be.visible');
     cy.get('.toc-link').should('be.visible');
 
 
+    // eslint-disable-next-line cypress/no-unnecessary-waiting
+    cy.wait(2000); // wait for calcViewHeight and rendering
+
+    // same screenshot is taken in access-to-page.spec
     cy.screenshot(`${ssPrefix}-visit-home`);
     cy.screenshot(`${ssPrefix}-visit-home`);
   });
   });