Explorar el Código

Merge branch 'master' into fix/107996-tag-operations-page-rename-vrt

Shun Miyazawa hace 3 años
padre
commit
ed94e2bc19
Se han modificado 41 ficheros con 214 adiciones y 143 borrados
  1. 1 1
      packages/app/src/components/Admin/UserGroup/UserGroupDropdown.tsx
  2. 39 24
      packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx
  3. 3 7
      packages/app/src/components/Admin/UserGroupDetail/UserGroupUserTable.tsx
  4. 2 1
      packages/app/src/components/Comments.tsx
  5. 2 1
      packages/app/src/components/DescendantsPageList.tsx
  6. 2 1
      packages/app/src/components/Fab.tsx
  7. 1 1
      packages/app/src/components/Hotkeys/Subscribers/CreatePage.jsx
  8. 9 6
      packages/app/src/components/LoginForm.tsx
  9. 2 1
      packages/app/src/components/Navbar/GlobalSearch.tsx
  10. 2 1
      packages/app/src/components/Navbar/GrowiNavbar.tsx
  11. 2 1
      packages/app/src/components/Navbar/GrowiNavbarBottom.tsx
  12. 2 1
      packages/app/src/components/Page.tsx
  13. 2 2
      packages/app/src/components/Page/DisplaySwitcher.tsx
  14. 1 2
      packages/app/src/components/PageAlert/TrashPageAlert.tsx
  15. 2 1
      packages/app/src/components/PageComment/CommentEditor.tsx
  16. 2 2
      packages/app/src/components/PageEditor.tsx
  17. 2 1
      packages/app/src/components/PageEditor/EditorNavbarBottom.tsx
  18. 1 1
      packages/app/src/components/PageEditor/LinkEditModal.jsx
  19. 6 6
      packages/app/src/components/PageEditorByHackmd.tsx
  20. 1 1
      packages/app/src/components/PageList/PageListItemL.tsx
  21. 1 1
      packages/app/src/components/PageTimeline.tsx
  22. 1 1
      packages/app/src/components/RevisionComparer/RevisionComparer.tsx
  23. 2 1
      packages/app/src/components/SavePageControls.tsx
  24. 2 1
      packages/app/src/components/Sidebar/PageTree.tsx
  25. 3 8
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  26. 17 6
      packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx
  27. 1 1
      packages/app/src/components/TableOfContents.tsx
  28. 3 2
      packages/app/src/components/User/UserDate.jsx
  29. 1 2
      packages/app/src/pages/[[...path]].page.tsx
  30. 15 4
      packages/app/src/pages/admin/user-group-detail/[userGroupId].page.tsx
  31. 1 5
      packages/app/src/pages/installer.page.tsx
  32. 1 2
      packages/app/src/pages/share/[[...path]].page.tsx
  33. 1 2
      packages/app/src/pages/trash.page.tsx
  34. 18 14
      packages/app/src/server/routes/login-passport.js
  35. 0 19
      packages/app/src/stores/context.tsx
  36. 1 1
      packages/app/src/stores/page-listing.tsx
  37. 1 1
      packages/app/src/stores/page-redirect.tsx
  38. 45 3
      packages/app/src/stores/page.tsx
  39. 3 1
      packages/app/src/stores/renderer.tsx
  40. 9 2
      packages/app/src/stores/ui.tsx
  41. 4 4
      packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts

+ 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 { IUserGroupHasId } from '~/interfaces/user';
+import type { IUserGroupHasId } from '~/interfaces/user';
 
 type Props = {
   selectableUserGroups?: IUserGroupHasId[]

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

@@ -5,6 +5,7 @@ import React, {
 import { objectIdUtils } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
+import Link from 'next/link';
 import { useRouter } from 'next/router';
 
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
@@ -18,7 +19,7 @@ import { useIsAclEnabled } from '~/stores/context';
 import { useUpdateUserGroupConfirmModal } from '~/stores/modal';
 import {
   useSWRxUserGroupPages, useSWRxUserGroupRelationList, useSWRxChildUserGroupList, useSWRxUserGroup,
-  useSWRxSelectableParentUserGroups, useSWRxSelectableChildUserGroups, useSWRxAncestorUserGroups,
+  useSWRxSelectableParentUserGroups, useSWRxSelectableChildUserGroups, useSWRxAncestorUserGroups, useSWRxUserGroupRelations,
 } from '~/stores/user-group';
 
 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: userGroupRelations, mutate: mutateUserGroupRelations } = useSWRxUserGroupRelations(currentUserGroupId);
 
   const { data: childUserGroupsList, mutate: mutateChildUserGroups } = useSWRxChildUserGroupList(currentUserGroupId ? [currentUserGroupId] : [], true);
   const childUserGroups = childUserGroupsList != null ? childUserGroupsList.childUserGroups : [];
   const grandChildUserGroups = childUserGroupsList != null ? childUserGroupsList.grandChildUserGroups : [];
   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 { 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 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,
       description: update.description,
       parentId: parentId ?? null,
       forceUpdateParents,
     });
-    const { userGroup: updatedUserGroup } = res.data;
 
     // mutate
+    mutateChildUserGroups();
     mutateAncestorUserGroups();
     mutateSelectableChildUserGroups();
     mutateSelectableParentUserGroups();
-  }, [mutateAncestorUserGroups, mutateSelectableChildUserGroups, mutateSelectableParentUserGroups]);
+  }, [mutateAncestorUserGroups, mutateChildUserGroups, mutateSelectableChildUserGroups, mutateSelectableParentUserGroups]);
 
   const onSubmitUpdateGroup = useCallback(
     async(targetGroup: IUserGroupHasId, userGroupData: Partial<IUserGroupHasId>, forceUpdateParents: boolean): Promise<void> => {
@@ -170,22 +172,28 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
   }, [currentUserGroupId, searchType, isAlsoMailSearched, isAlsoNameSearched]);
 
   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
   const removeUserByUsername = useCallback(async(username: string) => {
     try {
       await apiv3Delete(`/user-groups/${currentUserGroupId}/users/${username}`);
       toastSuccess(`Removed "${xss.process(username)}" from "${xss.process(currentUserGroup?.name)}"`);
-      mutateUserGroupRelations();
+      mutateUserGroupRelationList();
     }
     catch (err) {
       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) => {
     setUpdateModalShown(true);
@@ -319,19 +327,27 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
     <div>
       <nav aria-label="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>
-                  )}
-                </li>
-              ))
+                  </Link>
+                ) }
+              </li>
+            ))
             )
           }
         </ol>
@@ -347,8 +363,7 @@ const UserGroupDetailPage = (props: Props): JSX.Element => {
       </div>
       <h2 className="admin-setting-header mt-4">{t('user_group_management.user_list')}</h2>
       <UserGroupUserTable
-        userGroup={currentUserGroup}
-        userGroupRelations={childUserGroupRelations}
+        userGroupRelations={userGroupRelations}
         onClickPlusBtn={() => setIsUserGroupUserModalShown(true)}
         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 { 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 = {
-  userGroupRelations: IUserGroupRelation[],
-  userGroup: IUserGroupHasId,
+  userGroupRelations: IUserGroupRelationHasIdPopulatedUser[] | undefined,
   onClickRemoveUserBtn: (username: string) => Promise<void>,
   onClickPlusBtn: () => void,
 }
@@ -18,10 +16,8 @@ export const UserGroupUserTable = (props: Props): JSX.Element => {
   const { t } = useTranslation();
 
   const {
-    userGroup, onClickRemoveUserBtn, onClickPlusBtn,
+    userGroupRelations, onClickRemoveUserBtn, onClickPlusBtn,
   } = props;
-  const { data: userGroupRelations } = useSWRxUserGroupRelations(userGroup._id);
-
 
   return (
     <table className="table table-bordered table-user-list">

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

@@ -5,8 +5,9 @@ import dynamic from 'next/dynamic';
 
 import { PageComment } from '~/components/PageComment';
 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';
 

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

@@ -11,8 +11,9 @@ import {
 import { IPagingResult } from '~/interfaces/paging-result';
 import { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
 import {
-  useIsGuestUser, useIsSharedUser, useIsTrashPage, useShowPageLimitationXL,
+  useIsGuestUser, useIsSharedUser, useShowPageLimitationXL,
 } from '~/stores/context';
+import { useIsTrashPage } from '~/stores/page';
 import {
   usePageTreeTermManager, useDescendantsPageListForCurrentPathTermManager, useSWRxDescendantsPageListForCurrrentPath,
   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 { 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 { useCurrentPagePath } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
 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 { useCurrentPagePath } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
+import { useCurrentPagePath } from '~/stores/page';
 
 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]);
 
+  const resetRegisterErrors = useCallback(() => {
+    if (registerErrors.length === 0) return;
+    setRegisterErrors([]);
+  }, [registerErrors.length]);
+
   const handleRegisterFormSubmit = useCallback(async(e, requestPath) => {
     e.preventDefault();
     setEmailForRegistrationOrder('');
@@ -276,6 +281,9 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
     };
     try {
       const res = await apiv3Post(requestPath, { registerForm });
+
+      resetRegisterErrors();
+
       const { redirectTo } = res.data;
       router.push(redirectTo ?? '/');
 
@@ -291,12 +299,7 @@ export const LoginForm = (props: LoginFormProps): JSX.Element => {
       }
     }
     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(() => {
     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 { IPageWithSearchMeta } from '~/interfaces/search';
 import {
-  useCurrentPagePath, useIsSearchScopeChildrenAsDefault, useIsSearchServiceReachable,
+  useIsSearchScopeChildrenAsDefault, useIsSearchServiceReachable,
 } from '~/stores/context';
+import { useCurrentPagePath } from '~/stores/page';
 import { useGlobalSearchFormRef } from '~/stores/ui';
 
 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 {
-  useIsSearchPage, useCurrentPagePath, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useCustomizedLogoSrc,
+  useIsSearchPage, useIsGuestUser, useIsSearchServiceConfigured, useAppTitle, useConfidential, useCustomizedLogoSrc,
 } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
+import { useCurrentPagePath } from '~/stores/page';
 import { useIsDeviceSmallerThanMd } from '~/stores/ui';
 
 import { HasChildren } from '../../interfaces/common';

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

@@ -1,7 +1,8 @@
 import React from 'react';
 
-import { useCurrentPagePath, useIsSearchPage } from '~/stores/context';
+import { useIsSearchPage } from '~/stores/context';
 import { usePageCreateModal } from '~/stores/modal';
+import { useCurrentPagePath } from '~/stores/page';
 import { useIsDeviceSmallerThanMd, useDrawerOpened } from '~/stores/ui';
 
 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 { getOptionsToSave } from '~/client/util/editor';
 import {
-  useIsGuestUser, useCurrentPageTocNode, useShareLinkId,
+  useIsGuestUser, useShareLinkId,
 } from '~/stores/context';
 import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
@@ -21,6 +21,7 @@ import {
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useViewOptions } from '~/stores/renderer';
 import {
+  useCurrentPageTocNode,
   useEditorMode, useIsMobile,
 } from '~/stores/ui';
 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 {
-  useCurrentPagePath, useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
+  useIsSharedUser, useIsEditable, useShareLinkId, useIsNotFound,
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
 import { EditorMode, useEditorMode } from '~/stores/ui';
 
 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 { useTranslation } from 'react-i18next';
 
-import { useIsTrashPage } from '~/stores/context';
 import { usePageDeleteModal, usePutBackPageModal } from '~/stores/modal';
-import { useSWRxPageInfo, useSWRxCurrentPage } from '~/stores/page';
+import { useSWRxPageInfo, useSWRxCurrentPage, useIsTrashPage } from '~/stores/page';
 import { useIsAbleToShowTrashPageManagementButtons } from '~/stores/ui';
 
 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 { useSWRxPageComment } from '~/stores/comment';
 import {
-  useCurrentPagePath, useCurrentUser, useRevisionId, useIsSlackConfigured,
+  useCurrentUser, useRevisionId, useIsSlackConfigured,
   useIsUploadableFile, useIsUploadableImage,
 } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
+import { useCurrentPagePath } from '~/stores/page';
 
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 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 { IEditorMethods } from '~/interfaces/editor-methods';
 import {
-  useCurrentPagePath, useCurrentPathname, useCurrentPageId,
+  useCurrentPathname, useCurrentPageId,
   useIsEditable, useIsIndentSizeForced, useIsUploadableFile, useIsUploadableImage, useEditingMarkdown,
 } from '~/stores/context';
 import {
   useCurrentIndentSize, useSWRxSlackChannels, useIsSlackEnabled, useIsTextlintEnabled, usePageTagsForEditors,
   useIsEnabledUnsavedWarning,
 } from '~/stores/editor';
-import { useSWRxCurrentPage } from '~/stores/page';
+import { useCurrentPagePath, useSWRxCurrentPage } from '~/stores/page';
 import { usePreviewOptions } from '~/stores/renderer';
 import {
   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 { useCurrentPagePath, useIsSlackConfigured } from '~/stores/context';
+import { useIsSlackConfigured } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled } from '~/stores/editor';
+import { useCurrentPagePath } from '~/stores/page';
 import {
   EditorMode, useDrawerOpened, useEditorMode, useIsDeviceSmallerThanMd,
 } 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 { apiv3Get } from '~/client/util/apiv3-client';
-import { useCurrentPagePath } from '~/stores/context';
+import { useCurrentPagePath } from '~/stores/page';
 
 import PagePreviewIcon from '../Icons/PagePreviewIcon';
 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 { IResHackmdIntegrated, IResHackmdDiscard } from '~/interfaces/hackmd';
 import {
-  useCurrentPagePath, useCurrentPageId, useHackmdUri,
+  useCurrentPageId, useCurrentPathname, useHackmdUri,
 } from '~/stores/context';
-import {
-  usePageIdOnHackmd, useHasDraftOnHackmd, useRevisionIdHackmdSynced, useRemoteRevisionId,
-} from '~/stores/hackmd';
 import {
   useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors, useIsEnabledUnsavedWarning,
 } 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 {
   EditorMode,
   useEditorMode, useSelectedGrant,
@@ -43,7 +43,7 @@ export const PageEditorByHackmd = (): JSX.Element => {
   const { t } = useTranslation();
   const { data: editorMode, mutate: mutateEditorMode } = useEditorMode();
   const { data: currentPagePath } = useCurrentPagePath();
-  const { data: currentPathname } = useCurrentPagePath();
+  const { data: currentPathname } = useCurrentPathname();
   const { data: slackChannelsData } = useSWRxSlackChannels(currentPagePath);
   const { data: isSlackEnabled } = useIsSlackEnabled();
   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>
                 ) }
                 { revisionShortBody != null && (
-                  <div>{revisionShortBody}</div>
+                  <div data-testid="revision-short-body-in-page-list-item-L">{revisionShortBody}</div>
                 ) }
                 {
                   !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 { IPageHasId } from '~/interfaces/page';
-import { useCurrentPagePath } from '~/stores/context';
+import { useCurrentPagePath } from '~/stores/page';
 import { useTimelineOptions } from '~/stores/renderer';
 
 import { RevisionLoader } from './Page/RevisionLoader';

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

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

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

@@ -11,9 +11,10 @@ import {
 import { CustomWindow } from '~/interfaces/global';
 import { IPageGrantData } from '~/interfaces/page';
 import {
-  useCurrentPagePath, useIsEditable, useCurrentPageId, useIsAclEnabled,
+  useIsEditable, useCurrentPageId, useIsAclEnabled,
 } from '~/stores/context';
 import { useIsEnabledUnsavedWarning } from '~/stores/editor';
+import { useCurrentPagePath } from '~/stores/page';
 import { useSelectedGrant } from '~/stores/ui';
 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 {
-  useCurrentPagePath, useCurrentPageId, useTargetAndAncestors, useIsGuestUser,
+  useCurrentPageId, useTargetAndAncestors, useIsGuestUser,
 } from '~/stores/context';
+import { useCurrentPagePath } from '~/stores/page';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 
 import ItemsTree from './PageTree/ItemsTree';

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

@@ -39,7 +39,7 @@ interface ItemProps {
   targetPathOrId?: Nullable<string>
   isOpen?: boolean
   isEnabledAttachTitleHeader?: boolean
-  onRenamed?(): void
+  onRenamed?(fromPath: string | undefined, toPath: string): void
   onClickDuplicateMenuItem?(pageToDuplicate: IPageForPageDuplicateModal): void
   onClickDeleteMenuItem?(pageToDelete: IPageToDeleteWithMeta): void
 }
@@ -191,7 +191,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       await mutateChildren();
 
       if (onRenamed != null) {
-        onRenamed();
+        onRenamed(page.path, newPagePath);
       }
 
       // force open
@@ -286,7 +286,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       });
 
       if (onRenamed != null) {
-        onRenamed();
+        onRenamed(page.path, newPagePath);
       }
 
       toastSuccess(t('renamed_pages', { path: page.path }));
@@ -380,11 +380,6 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const pathRecoveryMenuItemClickHandler = async(pageId: string): Promise<void> => {
     try {
       await resumeRenameOperation(pageId);
-
-      if (onRenamed != null) {
-        onRenamed();
-      }
-
       toastSuccess(t('page_operation.paths_recovered'));
     }
     catch {

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

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

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

@@ -1,7 +1,8 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
 import { format } from 'date-fns';
+import PropTypes from 'prop-types';
+
 
 /**
  * UserDate
@@ -15,7 +16,7 @@ export default class UserDate extends React.Component {
     const dt = format(date, this.props.format);
 
     return (
-      <span className={this.props.className}>
+      <span className={this.props.className} data-hide-in-vrt>
         {dt}
       </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 PageStatusAlert from '../client/js/components/PageStatusAlert';
 import {
-  useCurrentUser, useCurrentPagePath,
+  useCurrentUser,
   useIsLatestRevision,
   useIsForbidden, useIsNotFound, useIsSharedUser,
   useIsEnabledStaleNotification, useIsIdenticalPath,
@@ -239,7 +239,6 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
   useCurrentPageId(pageId ?? null);
   // useIsNotCreatable(props.isForbidden || !isCreatablePage(pagePath)); // TODO: need to include props.isIdentical
-  useCurrentPagePath(pagePath);
   useCurrentPathname(props.currentPathname);
 
   useSWRxCurrentPage(undefined, pageWithMeta?.data ?? null); // store initial data

+ 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 { useRouter } from 'next/router';
 
+import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CommonProps, useCustomTitle } from '~/pages/utils/commons';
+import { useIsAclEnabled } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
 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 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');
   useIsMaintenanceMode(props.isMaintenanceMode);
   const router = useRouter();
@@ -23,9 +28,10 @@ const AdminUserGroupDetailPage: NextPage<CommonProps> = (props) => {
   const title = t('user_group_management.user_group_management');
   const customTitle = useCustomTitle(props, title);
 
-
   const currentUserGroupId = Array.isArray(userGroupId) ? userGroupId[0] : userGroupId;
 
+  useIsAclEnabled(props.isAclEnabled);
+
   return (
     <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) => {
-  const props = await retrieveServerSideProps(context);
+  const props = await retrieveServerSideProps(context, injectServerConfigurations);
+
   return props;
 };
 
-
 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 {
-  useCurrentPagePath, useCsrfToken,
-  useAppTitle, useSiteUrl, useConfidential,
+  useCsrfToken, useAppTitle, useSiteUrl, useConfidential,
 } from '../stores/context';
 
 
@@ -40,9 +39,6 @@ const InstallerPage: NextPage<Props> = (props: Props) => {
   useConfidential(props.confidential);
   useCsrfToken(props.csrfToken);
 
-  // page
-  useCurrentPagePath(props.currentPathname);
-
   const classNames: string[] = [];
 
   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 { IShareLinkHasId } from '~/interfaces/share-link';
 import {
-  useCurrentUser, useCurrentPagePath, useCurrentPathname, useCurrentPageId, useRendererConfig, useIsSearchPage,
+  useCurrentUser, useCurrentPathname, useCurrentPageId, useRendererConfig, useIsSearchPage,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault,
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
@@ -50,7 +50,6 @@ const SharedPage: NextPage<Props> = (props: Props) => {
   useIsSearchPage(false);
   useShareLinkId(props.shareLink?._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
-  useCurrentPagePath(props.shareLink?.relatedPage.path);
   useCurrentUser(props.currentUser);
   useCurrentPathname(props.currentPathname);
   useRendererConfig(props.rendererConfig);

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

@@ -16,7 +16,7 @@ import {
 
 import { BasicLayout } from '../components/Layout/BasicLayout';
 import {
-  useCurrentUser, useCurrentPageId, useCurrentPagePath, useCurrentPathname,
+  useCurrentUser, useCurrentPageId, useCurrentPathname,
   useIsSearchServiceConfigured, useIsSearchServiceReachable,
   useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL, useIsGuestUser,
 } from '../stores/context';
@@ -52,7 +52,6 @@ const TrashPage: NextPage<CommonProps> = (props: Props) => {
   useIsSearchPage(false);
   useCurrentPageId(null);
   useCurrentPathname('/trash');
-  useCurrentPagePath('/trash');
 
   // UserUISettings
   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 {*} res
    */
-  const loginSuccessHandler = async(req, res, user, action) => {
+  const loginSuccessHandler = async(req, res, user, action, isExternalAccount = false) => {
 
     // update lastLoginAt
     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 = {
       ip:  req.ip,
       endpoint: req.originalUrl,
@@ -124,6 +118,16 @@ module.exports = function(crowi, app) {
 
     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 });
   };
 
@@ -253,7 +257,7 @@ module.exports = function(crowi, app) {
         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) => {
       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) => {
       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) => {
       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) => {
       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 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) => {
       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);
     });
   };
 

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

@@ -52,10 +52,6 @@ export const useRevisionId = (initialData?: Nullable<any>): SWRResponse<Nullable
   return useContextSWR<Nullable<any>, Error>('revisionId', initialData);
 };
 
-export const useCurrentPagePath = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
-  return useContextSWR<Nullable<string>, Error>('currentPagePath', initialData);
-};
-
 export const useCurrentPathname = (initialData?: string): SWRResponse<string, Error> => {
   return useContextSWR('currentPathname', initialData);
 };
@@ -251,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,
 } from '../interfaces/page-listing-results';
 
-import { useCurrentPagePath } from './context';
+import { useCurrentPagePath } from './page';
 import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
 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 { useCurrentPagePath } from './context';
+import { useCurrentPagePath } from './page';
 import { useStaticSWR } from './use-static-swr';
 
 type RedirectFromUtil = {

+ 45 - 3
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 { apiGet } from '~/client/util/apiv1-client';
@@ -12,7 +15,10 @@ import { IRevisionsForPagination } from '~/interfaces/revision';
 
 import { IPageTagsInfo } from '../interfaces/tag';
 
-import { useCurrentPageId } from './context';
+import { useCurrentPageId, useCurrentPathname } from './context';
+
+
+const { isPermalink: _isPermalink } = pagePathUtils;
 
 
 export const useSWRxPage = (pageId?: string|null, shareLinkId?: string): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
@@ -140,3 +146,39 @@ export const useSWRxApplicableGrant = (
     (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 {
-  useCurrentPagePath, useCurrentPageTocNode, useRendererConfig,
+  useRendererConfig,
 } from './context';
+import { useCurrentPagePath } from './page';
+import { useCurrentPageTocNode } from './ui';
 
 interface ReactMarkdownOptionsGenerator {
   (config: RendererConfig): RendererOptions

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

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

+ 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', () => {
-     cy.visit('/Sandbox/Bootstrap4', {  });
+     cy.visit('/Sandbox/Bootstrap4');
      cy.get('#grw-subnav-container').within(() => {
       cy.getByTestid('open-page-item-control-btn').within(() => {
         cy.get('button.btn-page-item-control').click({force: true});
@@ -314,9 +314,9 @@ context('Tag Oprations', () =>{
           cy.wrap($row).within(() => {
             cy.getByTestid('open-page-item-control-btn').first().click();
             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`);
             })
           });
         }