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

fix: enhance subnavigation handling for empty pages

Yuki Takei 3 месяцев назад
Родитель
Сommit
15efa7f537

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

@@ -270,7 +270,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const revisionId = (revision != null && isPopulated(revision)) ? revision._id : undefined;
 
   const { editorMode } = useEditorMode();
-  const pageId = useCurrentPageId();
+  const pageId = useCurrentPageId(true);
   const currentUser = useCurrentUser();
   const isGuestUser = useIsGuestUser();
   const isReadOnlyUser = useIsReadOnlyUser();
@@ -291,7 +291,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
   const [isStickyActive, setStickyActive] = useState(false);
 
-
   const path = currentPage?.path ?? currentPathname;
   // const grant = currentPage?.grant ?? grantData?.grant;
   // const grantUserGroupId = currentPage?.grantedGroup?._id ?? grantData?.grantedGroup?.id;

+ 14 - 20
apps/app/src/client/components/PageControls/PageControls.tsx

@@ -3,9 +3,12 @@ import React, {
 } from 'react';
 
 import type {
+  IPageInfoForEmpty,
   IPageInfoForOperation, IPageToDeleteWithMeta, IPageToRenameWithMeta,
 } from '@growi/core';
 import {
+  isIPageInfoForEmpty,
+
   isIPageInfoForEntity, isIPageInfoForOperation,
 } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
@@ -121,7 +124,7 @@ type CommonProps = {
 }
 
 type PageControlsSubstanceProps = CommonProps & {
-  pageInfo: IPageInfoForOperation,
+  pageInfo: IPageInfoForOperation | IPageInfoForEmpty,
   onClickEditTagsButton: () => void,
 }
 
@@ -287,21 +290,12 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
     return wideviewMenuItemRenderer;
   }, [pageInfo, expandContentWidth, onClickSwitchContentWidth, switchContentWidthClickHandler]);
 
-  if (!isIPageInfoForEntity(pageInfo)) {
-    return <></>;
-  }
-
-  const {
-    sumOfLikers, sumOfSeenUsers, isLiked,
-  } = pageInfo;
-
   const forceHideMenuItemsWithAdditions = [
     ...(forceHideMenuItems ?? []),
     MenuItemType.BOOKMARK,
     MenuItemType.REVERT,
   ];
 
-  const _isIPageInfoForOperation = isIPageInfoForOperation(pageInfo);
   const isViewMode = editorMode === EditorMode.View;
 
   return (
@@ -313,7 +307,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
         </>
       )}
 
-      {revisionId != null && !isViewMode && _isIPageInfoForOperation && (
+      {revisionId != null && !isViewMode && (
         <Tags
           onClickEditTagsButton={onClickEditTagsButton}
         />
@@ -321,38 +315,38 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
 
       {!hideSubControls && (
         <div className={`hstack gap-1 ${!isViewMode && 'd-none d-lg-flex'}`}>
-          {revisionId != null && _isIPageInfoForOperation && (
+          {isIPageInfoForEntity(pageInfo) && revisionId != null && (
             <SubscribeButton
               status={pageInfo.subscriptionStatus}
               onClick={subscribeClickhandler}
             />
           )}
-          {revisionId != null && _isIPageInfoForOperation && (
+          {isIPageInfoForEntity(pageInfo) && revisionId != null && (
             <LikeButtons
               onLikeClicked={likeClickhandler}
-              sumOfLikers={sumOfLikers}
-              isLiked={isLiked}
+              sumOfLikers={pageInfo.sumOfLikers}
+              isLiked={pageInfo.isLiked}
               likers={likers}
             />
           )}
-          {revisionId != null && _isIPageInfoForOperation && (
+          {revisionId != null && (
             <BookmarkButtons
               pageId={pageId}
               isBookmarked={pageInfo.isBookmarked}
               bookmarkCount={pageInfo.bookmarkCount}
             />
           )}
-          {revisionId != null && !isSearchPage && (
+          {isIPageInfoForEntity(pageInfo) && revisionId != null && !isSearchPage && (
             <SeenUserInfo
               seenUsers={seenUsers}
-              sumOfSeenUsers={sumOfSeenUsers}
+              sumOfSeenUsers={pageInfo.sumOfSeenUsers}
               disabled={disableSeenUserInfoPopover}
             />
           )}
         </div>
       )}
 
-      {showPageControlDropdown && _isIPageInfoForOperation && (
+      {showPageControlDropdown && (
         <PageItemControl
           pageId={pageId}
           pageInfo={pageInfo}
@@ -393,7 +387,7 @@ export const PageControls = memo((props: PageControlsProps): JSX.Element => {
     return <></>;
   }
 
-  if (!isIPageInfoForEntity(pageInfo)) {
+  if (!isIPageInfoForOperation(pageInfo) && !isIPageInfoForEmpty(pageInfo)) {
     return <></>;
   }
 

+ 8 - 2
apps/app/src/states/page/hooks.ts

@@ -10,7 +10,8 @@ import { useIsGuestUser, useIsReadOnlyUser } from '../context';
 import { useCurrentPathname } from '../global';
 import {
   currentPageDataAtom,
-  currentPageIdAtom,
+  currentPageEmptyIdAtom,
+  currentPageEntityIdAtom,
   currentPagePathAtom,
   isForbiddenAtom,
   isIdenticalPathAtom,
@@ -33,7 +34,12 @@ import {
  */
 
 // Read-only hooks for page state
-export const useCurrentPageId = () => useAtomValue(currentPageIdAtom);
+export const useCurrentPageId = (includeEmpty: boolean = false) => {
+  const entityPageId = useAtomValue(currentPageEntityIdAtom);
+  const emptyPageId = useAtomValue(currentPageEmptyIdAtom);
+
+  return includeEmpty ? (entityPageId ?? emptyPageId) : entityPageId;
+};
 
 export const useCurrentPageData = () => useAtomValue(currentPageDataAtom);
 

+ 8 - 2
apps/app/src/states/page/hydrate.ts

@@ -3,13 +3,15 @@ import {
   type IPageNotFoundInfo,
   type IPagePopulatedToShowRevision,
   isIPageInfo,
+  isIPageInfoForEmpty,
   isIPageNotFoundInfo,
 } from '@growi/core';
 import { useHydrateAtoms } from 'jotai/utils';
 
 import {
   currentPageDataAtom,
-  currentPageIdAtom,
+  currentPageEmptyIdAtom,
+  currentPageEntityIdAtom,
   isForbiddenAtom,
   isIdenticalPathAtom,
   pageNotFoundAtom,
@@ -56,7 +58,7 @@ export const useHydratePageAtoms = (
 ): void => {
   useHydrateAtoms([
     // Core page state - automatically extract from page object
-    [currentPageIdAtom, page?._id],
+    [currentPageEntityIdAtom, page?._id],
     [currentPageDataAtom, page ?? undefined],
     [
       pageNotFoundAtom,
@@ -68,6 +70,10 @@ export const useHydratePageAtoms = (
       isForbiddenAtom,
       isIPageNotFoundInfo(pageMeta) ? pageMeta.isForbidden : false,
     ],
+    [
+      currentPageEmptyIdAtom,
+      isIPageInfoForEmpty(pageMeta) ? pageMeta.emptyPageId : undefined,
+    ],
 
     // Remote revision data - used by ConflictDiffModal
     [remoteRevisionBodyAtom, page?.revision?.body],

+ 6 - 4
apps/app/src/states/page/internal-atoms.ts

@@ -8,7 +8,8 @@ import { atom } from 'jotai';
  */
 
 // Core page state atoms (internal)
-export const currentPageIdAtom = atom<string>();
+export const currentPageEntityIdAtom = atom<string>();
+export const currentPageEmptyIdAtom = atom<string>();
 export const currentPageDataAtom = atom<IPagePopulatedToShowRevision>();
 export const pageNotFoundAtom = atom(false);
 export const isIdenticalPathAtom = atom<boolean>(false);
@@ -46,7 +47,7 @@ const untitledPageStateAtom = atom<boolean>(false);
 // Derived atom for untitled page state with currentPageId dependency
 export const isUntitledPageAtom = atom(
   (get) => {
-    const currentPageId = get(currentPageIdAtom);
+    const currentPageId = get(currentPageEntityIdAtom);
     // If no current page ID exists, return false (no page loaded)
     if (currentPageId == null) {
       return false;
@@ -55,7 +56,7 @@ export const isUntitledPageAtom = atom(
     return get(untitledPageStateAtom);
   },
   (get, set, newValue: boolean) => {
-    const currentPageId = get(currentPageIdAtom);
+    const currentPageId = get(currentPageEntityIdAtom);
     // Only update state if current page ID exists
     if (currentPageId != null) {
       set(untitledPageStateAtom, newValue);
@@ -124,7 +125,8 @@ export const _atomsForDerivedAbilities = {
   currentPagePathAtom,
   isIdenticalPathAtom,
   shareLinkIdAtom,
-  currentPageIdAtom,
+  currentPageEntityIdAtom,
+  currentPageEmptyIdAtom,
   isTrashPageAtom,
 } as const;
 

+ 14 - 5
apps/app/src/states/page/use-fetch-current-page.ts

@@ -1,6 +1,7 @@
 import { useCallback } from 'react';
 import {
   type IPagePopulatedToShowRevision,
+  isIPageInfoForEmpty,
   isIPageNotFoundInfo,
 } from '@growi/core';
 import { isErrorV3 } from '@growi/core/dist/models';
@@ -16,7 +17,8 @@ import loggerFactory from '~/utils/logger';
 
 import {
   currentPageDataAtom,
-  currentPageIdAtom,
+  currentPageEmptyIdAtom,
+  currentPageEntityIdAtom,
   isForbiddenAtom,
   pageErrorAtom,
   pageLoadingAtom,
@@ -176,7 +178,7 @@ export const useFetchCurrentPage = (): {
   error: Error | null;
 } => {
   const shareLinkId = useAtomValue(shareLinkIdAtom);
-  const currentPageId = useAtomValue(currentPageIdAtom);
+  const currentPageId = useAtomValue(currentPageEntityIdAtom);
 
   const isLoading = useAtomValue(pageLoadingAtom);
   const error = useAtomValue(pageErrorAtom);
@@ -193,7 +195,7 @@ export const useFetchCurrentPage = (): {
         set,
         args?: FetchPageArgs,
       ): Promise<IPagePopulatedToShowRevision | null> => {
-        const currentPageId = get(currentPageIdAtom);
+        const currentPageId = get(currentPageEntityIdAtom);
         const currentPageData = get(currentPageDataAtom);
         const revisionIdFromUrl = get(revisionIdFromUrlAtom);
 
@@ -237,7 +239,8 @@ export const useFetchCurrentPage = (): {
           const { page: newData } = data;
 
           set(currentPageDataAtom, newData);
-          set(currentPageIdAtom, newData._id);
+          set(currentPageEntityIdAtom, newData._id);
+          set(currentPageEmptyIdAtom, undefined);
           set(pageNotFoundAtom, false);
           set(isForbiddenAtom, false);
 
@@ -260,7 +263,13 @@ export const useFetchCurrentPage = (): {
               set(pageNotFoundAtom, true);
               set(isForbiddenAtom, error.args.isForbidden ?? false);
               set(currentPageDataAtom, undefined);
-              set(currentPageIdAtom, undefined);
+              set(currentPageEntityIdAtom, undefined);
+              set(
+                currentPageEmptyIdAtom,
+                isIPageInfoForEmpty(error.args)
+                  ? error.args.emptyPageId
+                  : undefined,
+              );
               set(remoteRevisionBodyAtom, undefined);
             }
           }

+ 1 - 1
apps/app/src/states/socket-io/global-socket.ts

@@ -68,7 +68,7 @@ export const useSetupGlobalSocket = (): void => {
  */
 export const useSetupGlobalSocketForPage = (): void => {
   const socket = useAtomValue(globalSocketAtom);
-  const pageId = useCurrentPageId();
+  const pageId = useCurrentPageId(true);
 
   useEffect(() => {
     if (socket == null || pageId == null) {

+ 9 - 29
apps/app/src/states/ui/page-abilities.ts

@@ -42,7 +42,9 @@ const isAbleToShowTagLabelAtom = atom((get) => {
 
   // "/trash" page does not exist on page collection and unable to add tags
   return (
+    // biome-ignore lint/style/noNonNullAssertion: currentPagePath should be defined here
     !isUsersTopPage(currentPagePath!) &&
+    // biome-ignore lint/style/noNonNullAssertion: currentPagePath should be defined here
     !isTrashTopPage(currentPagePath!) &&
     shareLinkId == null &&
     !isIdenticalPath &&
@@ -60,22 +62,13 @@ export const useIsAbleToShowTagLabel = (): boolean => {
 // Derived atom for TrashPageManagementButtons display ability
 const isAbleToShowTrashPageManagementButtonsAtom = atom((get) => {
   const currentUser = get(globalAtoms.currentUserAtom);
-  const currentPageId = get(pageAtoms.currentPageIdAtom);
-  const isNotFound = get(pageAtoms.pageNotFoundAtom);
+  const currentPageEntityId = get(pageAtoms.currentPageEntityIdAtom);
+  const currentPageEmptyId = get(pageAtoms.currentPageEmptyIdAtom);
   const isTrashPage = get(pageAtoms.isTrashPageAtom);
   const isReadOnlyUser = get(contextAtoms.isReadOnlyUserAtom);
 
-  // Return false if any dependency is undefined
-  if (
-    [currentUser, currentPageId, isNotFound, isReadOnlyUser, isTrashPage].some(
-      (v) => v === undefined,
-    )
-  ) {
-    return false;
-  }
-
   const isCurrentUserExist = currentUser != null;
-  const isPageExist = currentPageId != null && isNotFound === false;
+  const isPageExist = currentPageEntityId != null || currentPageEmptyId != null;
   const isTrashPageCondition = isPageExist && isTrashPage === true;
   const isReadOnlyUserCondition = isPageExist && isReadOnlyUser === true;
 
@@ -91,29 +84,16 @@ export const useIsAbleToShowTrashPageManagementButtons = (): boolean => {
 
 // Derived atom for PageManagement display ability
 const isAbleToShowPageManagementAtom = atom((get) => {
-  const currentPageId = get(pageAtoms.currentPageIdAtom);
-  const isNotFound = get(pageAtoms.pageNotFoundAtom);
+  const currentPageEntityId = get(pageAtoms.currentPageEntityIdAtom);
+  const currentPageEmptyId = get(pageAtoms.currentPageEmptyIdAtom);
   const isTrashPage = get(pageAtoms.isTrashPageAtom);
   const isSharedUser = get(contextAtoms.isSharedUserAtom);
 
-  const pageId = currentPageId;
-
-  // Return false if any dependency is undefined
-  if (
-    [pageId, isTrashPage, isSharedUser, isNotFound].some((v) => v === undefined)
-  ) {
-    return false;
-  }
-
-  const isPageExist = pageId != null && isNotFound === false;
-  const isEmptyPage = pageId != null && isNotFound === true;
+  const isPageExist = currentPageEntityId != null || currentPageEmptyId != null;
   const isTrashPageCondition = isPageExist && isTrashPage === true;
   const isSharedUserCondition = isPageExist && isSharedUser === true;
 
-  return (
-    (isPageExist && !isTrashPageCondition && !isSharedUserCondition) ||
-    isEmptyPage
-  );
+  return isPageExist && !isTrashPageCondition && !isSharedUserCondition;
 });
 
 /**