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

Merge pull request #5661 from weseek/imprv/remove-toc-in-empty-page

imprv: Remove toc and page authors in empty page
Yohei Shiina 3 лет назад
Родитель
Сommit
bf2d047143

+ 5 - 2
packages/app/src/client/services/ContextExtractor.tsx

@@ -18,7 +18,7 @@ import {
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useCurrentUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
   useNotFoundTargetPathOrId, useIsSearchPage, useIsForbidden, useIsIdenticalPath, useHasParent,
   useIsAclEnabled, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsEnabledAttachTitleHeader, useIsNotFoundPermalink,
-  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken,
+  useDefaultIndentSize, useIsIndentSizeForced, useCsrfToken, useIsEmptyPageInNotFoundContext,
 } from '../../stores/context';
 
 const { isTrashPage: _isTrashPage } = pagePathUtils;
@@ -57,7 +57,8 @@ const ContextExtractorOnce: FC = () => {
    */
   const revisionId = mainContent?.getAttribute('data-page-revision-id');
   const path = decodeURI(mainContent?.getAttribute('data-path') || '');
-  const pageId = mainContent?.getAttribute('data-page-id') || null;
+  const pageId = mainContent?.getAttribute('data-page-id') || notFoundContent?.getAttribute('data-page-id');
+
   const revisionCreatedAt = +(mainContent?.getAttribute('data-page-revision-created') || '');
 
   // createdAt
@@ -91,6 +92,7 @@ const ContextExtractorOnce: FC = () => {
   const notFoundTargetPathOrId = JSON.parse(notFoundContentForPt?.getAttribute('data-not-found-target-path-or-id') || jsonNull);
   const isNotFoundPermalink = JSON.parse(notFoundContent?.getAttribute('data-is-not-found-permalink') || jsonNull);
   const isSearchPage = document.getElementById('search-page') != null;
+  const isEmptyPage = notFoundContent != null && pageId != null;
 
   const grant = +(mainContent?.getAttribute('data-page-grant') || 1);
   const grantGroupId = mainContent?.getAttribute('data-page-grant-group') || null;
@@ -151,6 +153,7 @@ const ContextExtractorOnce: FC = () => {
   useNotFoundTargetPathOrId(notFoundTargetPathOrId);
   useIsNotFoundPermalink(isNotFoundPermalink);
   useIsSearchPage(isSearchPage);
+  useIsEmptyPageInNotFoundContext(isEmptyPage);
   useHasParent(hasParent);
 
   // Navigation

+ 21 - 11
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -8,7 +8,9 @@ import EditorContainer from '~/client/services/EditorContainer';
 import { exportAsMarkdown } from '~/client/services/page-operation';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
-import { IPageHasId, IPageToRenameWithMeta, IPageWithMeta } from '~/interfaces/page';
+import {
+  IPageHasId, IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity,
+} from '~/interfaces/page';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import {
   useCurrentCreatedAt, useCurrentUpdatedAt, useCurrentPageId, useRevisionId, useCurrentPagePath,
@@ -208,8 +210,12 @@ const GrowiContextualSubNavigation = (props) => {
     openDuplicateModal(page, { onDuplicated: duplicatedHandler });
   }, [openDuplicateModal]);
 
-  const renameItemClickedHandler = useCallback(async(page: IPageToRenameWithMeta) => {
+  const renameItemClickedHandler = useCallback(async(page: IPageToRenameWithMeta<IPageInfoForEntity>) => {
     const renamedHandler: OnRenamedFunction = () => {
+      if (page.data._id !== null) {
+        window.location.href = `/${page.data._id}`;
+        return;
+      }
       window.location.reload();
     };
     openRenameModal(page, { onRenamed: renamedHandler });
@@ -245,6 +251,18 @@ const GrowiContextualSubNavigation = (props) => {
       mutateEditorMode(viewType);
     }
 
+    let additionalMenuItemsRenderer;
+    if (revisionId != null) {
+      additionalMenuItemsRenderer = props => (
+        <AdditionalMenuItems
+          {...props}
+          pageId={pageId}
+          revisionId={revisionId}
+          isLinkSharingDisabled={isLinkSharingDisabled}
+          onClickTemplateMenuItem={templateMenuItemClickHandler}
+        />
+      );
+    }
     return (
       <>
         <div className="d-flex flex-column align-items-end justify-content-center py-md-2" style={{ gap: `${isCompactMode ? '5px' : '7px'}` }}>
@@ -258,15 +276,7 @@ const GrowiContextualSubNavigation = (props) => {
                 path={path}
                 disableSeenUserInfoPopover={isSharedUser}
                 showPageControlDropdown={isAbleToShowPageManagement}
-                additionalMenuItemRenderer={props => (
-                  <AdditionalMenuItems
-                    {...props}
-                    pageId={pageId}
-                    revisionId={revisionId}
-                    isLinkSharingDisabled={isLinkSharingDisabled}
-                    onClickTemplateMenuItem={templateMenuItemClickHandler}
-                  />
-                )}
+                additionalMenuItemRenderer={additionalMenuItemsRenderer}
                 onClickDuplicateMenuItem={duplicateItemClickedHandler}
                 onClickRenameMenuItem={renameItemClickedHandler}
                 onClickDeleteMenuItem={deleteItemClickedHandler}

+ 29 - 24
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -33,7 +33,7 @@ type CommonProps = {
 type SubNavButtonsSubstanceProps = CommonProps & {
   pageId: string,
   shareLinkId?: string | null,
-  revisionId: string,
+  revisionId: string | null,
   path?: string | null,
   pageInfo: IPageInfoAll,
 }
@@ -154,27 +154,33 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
 
   return (
     <div className="d-flex" style={{ gap: '2px' }}>
-      <span>
-        <SubscribeButton
-          status={pageInfo.subscriptionStatus}
-          onClick={subscribeClickhandler}
+      {revisionId != null && (
+        <span>
+          <SubscribeButton
+            status={pageInfo.subscriptionStatus}
+            onClick={subscribeClickhandler}
+          />
+        </span>
+      )}
+      {revisionId != null && (
+        <LikeButtons
+          hideTotalNumber={isCompactMode}
+          onLikeClicked={likeClickhandler}
+          sumOfLikers={sumOfLikers}
+          isLiked={isLiked}
+          likers={likers}
+        />
+      )}
+      {revisionId != null && (
+        <BookmarkButtons
+          hideTotalNumber={isCompactMode}
+          bookmarkCount={bookmarkCount}
+          isBookmarked={isBookmarked}
+          bookmarkedUsers={bookmarkInfo?.bookmarkedUsers}
+          onBookMarkClicked={bookmarkClickHandler}
         />
-      </span>
-      <LikeButtons
-        hideTotalNumber={isCompactMode}
-        onLikeClicked={likeClickhandler}
-        sumOfLikers={sumOfLikers}
-        isLiked={isLiked}
-        likers={likers}
-      />
-      <BookmarkButtons
-        hideTotalNumber={isCompactMode}
-        bookmarkCount={bookmarkCount}
-        isBookmarked={isBookmarked}
-        bookmarkedUsers={bookmarkInfo?.bookmarkedUsers}
-        onBookMarkClicked={bookmarkClickHandler}
-      />
-      { !isCompactMode && (
+      )}
+      {revisionId != null && !isCompactMode && (
         <SeenUserInfo
           seenUsers={seenUsers}
           sumOfSeenUsers={sumOfSeenUsers}
@@ -212,7 +218,7 @@ export const SubNavButtons = (props: SubNavButtonsProps): JSX.Element => {
 
   const { data: pageInfo, error } = useSWRxPageInfo(pageId ?? null, shareLinkId);
 
-  if (revisionId == null || error != null) {
+  if (error != null) {
     return <></>;
   }
 
@@ -220,13 +226,12 @@ export const SubNavButtons = (props: SubNavButtonsProps): JSX.Element => {
     return <></>;
   }
 
-
   return (
     <SubNavButtonsSubstance
       {...props}
       pageInfo={pageInfo}
       pageId={pageId}
-      revisionId={revisionId}
+      revisionId={revisionId ?? null}
       path={path}
       onClickDuplicateMenuItem={onClickDuplicateMenuItem}
       onClickRenameMenuItem={onClickRenameMenuItem}

+ 30 - 3
packages/app/src/components/NotFoundPage.tsx

@@ -1,15 +1,42 @@
-import React, { useMemo } from 'react';
+import React, { useMemo, useEffect } from 'react';
+
 import { useTranslation } from 'react-i18next';
+import urljoin from 'url-join';
+
+import { useCurrentPageId, useCurrentPagePath, useNotFoundTargetPathOrId } from '~/stores/context';
 
-import PageListIcon from './Icons/PageListIcon';
-import TimeLineIcon from './Icons/TimeLineIcon';
 import CustomNavAndContents from './CustomNavigation/CustomNavAndContents';
 import { DescendantsPageListForCurrentPath } from './DescendantsPageList';
+import PageListIcon from './Icons/PageListIcon';
+import TimeLineIcon from './Icons/TimeLineIcon';
 import PageTimeline from './PageTimeline';
 
+/**
+ * Replace url in address bar with new path and query parameters
+ */
+const replaceURLHistory = (path: string) => {
+  const queryParameters = window.location.search;
+  window.history.replaceState(null, '', urljoin(path, queryParameters));
+};
 
 const NotFoundPage = (): JSX.Element => {
   const { t } = useTranslation();
+  const { data: pageId } = useCurrentPageId();
+  const { data: path } = useCurrentPagePath();
+  const { data: notFoundTargetPathOrId } = useNotFoundTargetPathOrId();
+
+  // replace url in address bar with path when accessing empty page by permalink
+  useEffect(() => {
+    const isEmptyPage = pageId != null; // Todo: should be improved. https://redmine.weseek.co.jp/issues/98152
+    const isPathExist = path != null;
+    const isPathLink = notFoundTargetPathOrId?.includes('/');
+
+    if (isEmptyPage && isPathExist && !isPathLink) {
+      // The (as string) below is a workaround for the case. See the link. (Fixed in typescript version 4.4)
+      // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-4.html#control-flow-analysis-of-aliased-conditions-and-discriminants
+      replaceURLHistory(path as string);
+    }
+  }, [pageId, path, notFoundTargetPathOrId]);
 
   const navTabMapping = useMemo(() => {
     return {

+ 7 - 5
packages/app/src/components/Page/DisplaySwitcher.tsx

@@ -7,7 +7,7 @@ import { TabContent, TabPane } from 'reactstrap';
 
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import {
-  useCurrentPagePath, useIsSharedUser, useIsEditable, useCurrentPageId, useIsUserPage, usePageUser, useShareLinkId,
+  useCurrentPagePath, useIsSharedUser, useIsEditable, useCurrentPageId, useIsUserPage, usePageUser, useShareLinkId, useIsEmptyPageInNotFoundContext,
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
@@ -36,7 +36,7 @@ const DisplaySwitcher = (): JSX.Element => {
   // get element for smoothScroll
   const getCommentListDom = useMemo(() => { return document.getElementById('page-comments-list') }, []);
 
-
+  const { data: isEmptyPageInNotFoundContext } = useIsEmptyPageInNotFoundContext();
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isSharedUser } = useIsSharedUser();
@@ -97,9 +97,11 @@ const DisplaySwitcher = (): JSX.Element => {
                   ) }
 
                   <div className="d-none d-lg-block">
-                    <div id="revision-toc" className="revision-toc">
-                      <TableOfContents />
-                    </div>
+                    {!isEmptyPageInNotFoundContext && (
+                      <div id="revision-toc" className="revision-toc">
+                        <TableOfContents />
+                      </div>
+                    )}
                     <ContentLinkButtons />
                   </div>
 

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

@@ -94,7 +94,7 @@ const PageRenameModal = (): JSX.Element => {
     try {
       const response = await apiv3Put('/pages/rename', {
         pageId: _id,
-        revisionId: revision,
+        revisionId: revision ?? null,
         isRecursively: !_isV5Compatible ? isRenameRecursively : undefined,
         isRenameRedirect,
         updateMetadata: !isRemainMetadata,

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

@@ -292,8 +292,8 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       return;
     }
 
-    if (page._id == null || page.revision == null || page.path == null) {
-      throw Error('Any of _id, revision, and path must not be null.');
+    if (page._id == null || page.path == null) {
+      throw Error('_id and path must not be null.');
     }
 
     const pageToDelete: IPageToDeleteWithMeta = {

+ 3 - 3
packages/app/src/interfaces/page.ts

@@ -75,7 +75,7 @@ export type IPageInfoAll = IPageInfo | IPageInfoForEntity | IPageInfoForOperatio
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 export const isIPageInfoForEntity = (pageInfo: any | undefined): pageInfo is IPageInfoForEntity => {
-  return pageInfo != null && ('isEmpty' in pageInfo) && pageInfo.isEmpty === false;
+  return pageInfo != null;
 };
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -115,8 +115,8 @@ export type IDataWithMeta<D = unknown, M = unknown> = {
 
 export type IPageWithMeta<M = IPageInfoAll> = IDataWithMeta<IPageHasId, M>;
 
-export type IPageToDeleteWithMeta = IDataWithMeta<HasObjectId & (IPage | { path: string, revision: string }), IPageInfoForEntity | unknown>;
-export type IPageToRenameWithMeta = IPageToDeleteWithMeta;
+export type IPageToDeleteWithMeta<T = IPageInfoForEntity | unknown> = IDataWithMeta<HasObjectId & (IPage | { path: string, revision: string | null}), T>;
+export type IPageToRenameWithMeta<T = IPageInfoForEntity | unknown> = IPageToDeleteWithMeta<T>;
 
 export type IPageGrantData = {
   grant: number,

+ 1 - 1
packages/app/src/server/models/obsolete-page.js

@@ -421,7 +421,7 @@ export const getPageSchema = (crowi) => {
   };
 
   /**
-   * find pages that is match with `path` and its descendants whitch user is able to manage
+   * find pages that is match with `path` and its descendants which user is able to manage
    */
   pageSchema.statics.findManageableListWithDescendants = async function(page, user, option = {}, includeEmpty = false) {
     if (user == null) {

+ 2 - 2
packages/app/src/server/routes/apiv3/page.js

@@ -640,12 +640,12 @@ module.exports = (crowi) => {
     const { fromPath, toPath } = req.query;
 
     try {
-      const fromPage = await Page.findByPath(fromPath);
+      const fromPage = await Page.findByPath(fromPath, true);
       if (fromPage == null) {
         return res.apiv3Err(new ErrorV3('fromPage is Null'), 400);
       }
 
-      const fromPageDescendants = await Page.findManageableListWithDescendants(fromPage, req.user);
+      const fromPageDescendants = await Page.findManageableListWithDescendants(fromPage, req.user, {}, true);
 
       const toPathDescendantsArray = fromPageDescendants.map((subordinatedPage) => {
         return convertToNewAffiliationPath(fromPath, toPath, subordinatedPage.path);

+ 1 - 1
packages/app/src/server/routes/apiv3/pages.js

@@ -173,7 +173,7 @@ module.exports = (crowi) => {
     ],
     renamePage: [
       body('pageId').isMongoId().withMessage('pageId is required'),
-      body('revisionId').optional().isMongoId().withMessage('revisionId is required'), // required when v4
+      body('revisionId').optional({ nullable: true }).isMongoId().withMessage('revisionId is required'), // required when v4
       body('newPagePath').isLength({ min: 1 }).withMessage('newPagePath is required'),
       body('isRecursively').if(value => value != null).isBoolean().withMessage('isRecursively must be boolean'),
       body('isRenameRedirect').if(value => value != null).isBoolean().withMessage('isRenameRedirect must be boolean'),

+ 30 - 25
packages/app/src/server/routes/page.js

@@ -279,6 +279,13 @@ module.exports = function(crowi, app) {
     renderVars.isNotFoundPermalink = !isPath && !await Page.exists({ _id: pathOrId });
   }
 
+  async function addRenderVarsWhenEmptyPage(renderVars, pageId) {
+    if (pageId == null) {
+      return;
+    }
+    renderVars.pageId = pageId;
+  }
+
   function replacePlaceholdersOfTemplate(template, req) {
     if (req.user == null) {
       return '';
@@ -333,9 +340,10 @@ module.exports = function(crowi, app) {
     const offset = parseInt(req.query.offset) || 0;
     await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit, true);
     await addRenderVarsForPageTree(renderVars, pathOrId, req.user);
-
     await addRenderVarsWhenNotFound(renderVars, pathOrId);
-
+    if (req.pageId != null) {
+      await addRenderVarsWhenEmptyPage(renderVars, req.pageId);
+    }
     return res.render(view, renderVars);
   }
 
@@ -420,13 +428,9 @@ module.exports = function(crowi, app) {
 
     // empty page
     if (page.isEmpty) {
-      // redirect to page (path) url
-      const url = new URL('https://dummy.origin');
-      url.pathname = page.path;
-      Object.entries(req.query).forEach(([key, value], i) => {
-        url.searchParams.append(key, value);
-      });
-      return res.safeRedirect(urljoin(url.pathname, url.search));
+      req.pageId = page._id;
+      req.pagePath = page.path;
+      return _notFound(req, res);
     }
 
     const { path } = page; // this must exist
@@ -596,40 +600,41 @@ module.exports = function(crowi, app) {
   async function redirector(req, res, next, path) {
     const { redirectFrom } = req.query;
 
-    const builder = new PageQueryBuilder(Page.find({ path }));
-    await Page.addConditionToFilteringByViewerForList(builder, req.user, true);
+    const includeEmpty = true;
+    const builder = new PageQueryBuilder(Page.find({ path }), includeEmpty);
 
-    const pages = await builder.query.lean().clone().exec('find');
+    builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
 
-    if (pages.length >= 2) {
-
-      // populate to list
-      builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
-      const identicalPathPages = await builder.query.lean().exec('find');
+    await Page.addConditionToFilteringByViewerForList(builder, req.user, true);
+    const pages = await builder.query.lean().clone().exec('find');
+    const nonEmptyPages = pages.filter(p => !p.isEmpty);
 
+    if (nonEmptyPages.length >= 2) {
       return res.render('layout-growi/identical-path-page', {
-        identicalPathPages,
+        identicalPathPages: nonEmptyPages,
         redirectFrom,
         path,
       });
     }
 
-    if (pages.length === 1) {
+    if (nonEmptyPages.length === 1) {
+      const nonEmptyPage = nonEmptyPages[0];
       const url = new URL('https://dummy.origin');
-      url.pathname = `/${pages[0]._id}`;
+
+      url.pathname = `/${nonEmptyPage._id}`;
       Object.entries(req.query).forEach(([key, value], i) => {
         url.searchParams.append(key, value);
       });
       return res.safeRedirect(urljoin(url.pathname, url.search));
     }
 
-    // Exclude isEmpty page to handle _notFound or forbidden
-    const isForbidden = await Page.exists({ path, isEmpty: false });
-    if (isForbidden) {
-      req.isForbidden = true;
+    // Processing of nonEmptyPage is finished by the time this code is read
+    // If any pages exist then they should be empty
+    const emptyPage = pages[0];
+    if (emptyPage != null) {
+      req.pageId = emptyPage._id;
       return _notFound(req, res);
     }
-
     // redirect by PageRedirect
     const pageRedirect = await PageRedirect.findOne({ fromPath: path });
     if (pageRedirect != null) {

+ 3 - 6
packages/app/src/server/service/page.ts

@@ -1414,7 +1414,7 @@ class PageService {
       await Page.replaceTargetWithPage(page, null, true);
     }
 
-    // Delete target
+    // Delete target (only updating an existing document's properties )
     let deletedPage;
     if (!page.isEmpty) {
       deletedPage = await this.deleteNonEmptyTarget(page, user);
@@ -1492,9 +1492,6 @@ class PageService {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
     await Page.deleteOne({ _id: page._id, isEmpty: true });
-
-    // update descendantCount of ancestors' before removeLeafEmptyPages
-    await this.updateDescendantCountOfAncestors(page._id, -page.descendantCount, false);
   }
 
   async deleteRecursivelyMainOperation(page, user, pageOpId: ObjectIdLike): Promise<void> {
@@ -2179,8 +2176,8 @@ class PageService {
         isV5Compatible: true,
         isEmpty: true,
         isMovable,
-        isDeletable: false,
-        isAbleToDeleteCompletely: false,
+        isDeletable: true,
+        isAbleToDeleteCompletely: true,
         isRevertible: false,
       };
     }

+ 1 - 0
packages/app/src/server/views/layout-growi/not_found.html

@@ -11,6 +11,7 @@
   <div
     id="growi-not-found-context"
     data-is-not-found-permalink="{% if isNotFoundPermalink %}{{isNotFoundPermalink|json}}{% endif %}"
+    data-page-id="{%if pageId %}{{pageId.toString()}}{% endif %}"
   >
   </div>
   <div class="grw-container-convertible">

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

@@ -156,6 +156,9 @@ export const useIsEnabledAttachTitleHeader = (initialData?: boolean) : SWRRespon
   return useStaticSWR<boolean, Error>('isEnabledAttachTitleHeader', initialData);
 };
 
+export const useIsEmptyPageInNotFoundContext = (initialData?: boolean) : SWRResponse<boolean, Error> => {
+  return useStaticSWR<boolean, Error>('isEmptyPageInNotFoundContext', initialData);
+};
 export const useHasParent = (initialData?: boolean) : SWRResponse<boolean, Error> => {
   return useStaticSWR<boolean, Error>('hasParent', initialData);
 };

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

@@ -16,7 +16,7 @@ import { UpdateDescCountData } from '~/interfaces/websocket';
 import loggerFactory from '~/utils/logger';
 
 import {
-  useCurrentPageId, useCurrentPagePath, useIsEditable, useIsTrashPage, useIsUserPage, useIsGuestUser,
+  useCurrentPageId, useCurrentPagePath, useIsEditable, useIsTrashPage, useIsUserPage, useIsGuestUser, useIsEmptyPageInNotFoundContext,
   useIsNotCreatable, useIsSharedUser, useNotFoundTargetPathOrId, useIsForbidden, useIsIdenticalPath, useIsNotFoundPermalink, useCurrentUser, useIsDeleted,
 } from './context';
 import { localStorageMiddleware } from './middlewares/sync-to-storage';
@@ -388,12 +388,13 @@ export const useIsAbleToShowPageAuthors = (): SWRResponse<boolean, Error> => {
   const key = 'isAbleToShowPageAuthors';
   const { data: currentPageId } = useCurrentPageId();
   const { data: isUserPage } = useIsUserPage();
+  const { data: isEmptyPageInNotFoundContext } = useIsEmptyPageInNotFoundContext();
 
   const includesUndefined = [currentPageId, isUserPage].some(v => v === undefined);
   const isPageExist = currentPageId != null;
 
   return useSWRImmutable(
-    includesUndefined ? null : key,
+    (includesUndefined || isEmptyPageInNotFoundContext) ? null : key,
     () => isPageExist && !isUserPage,
   );
 };