Преглед изворни кода

Merge pull request #6363 from weseek/imprv/101293-empty-NotFoundPage

imprv: 101293 empty not found page
yuken пре 3 година
родитељ
комит
6c42bec7f7

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

@@ -7,7 +7,7 @@ import { TabContent, TabPane } from 'reactstrap';
 
 
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import { smoothScrollIntoView } from '~/client/util/smooth-scroll';
 import {
 import {
-  useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound,
+  useCurrentPagePath, useIsSharedUser, useIsEditable, useIsUserPage, usePageUser, useShareLinkId, useIsNotFound, useIsNotCreatable,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
@@ -48,6 +48,7 @@ const DisplaySwitcher = (): JSX.Element => {
   const { data: isEditable } = useIsEditable();
   const { data: isEditable } = useIsEditable();
   const { data: pageUser } = usePageUser();
   const { data: pageUser } = usePageUser();
   const { data: isNotFound } = useIsNotFound();
   const { data: isNotFound } = useIsNotFound();
+  const { data: isNotCreatable } = useIsNotCreatable();
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
   const { data: currentPage } = useSWRxCurrentPage(shareLinkId ?? undefined);
 
 
   const { data: editorMode } = useEditorMode();
   const { data: editorMode } = useEditorMode();
@@ -71,7 +72,7 @@ const DisplaySwitcher = (): JSX.Element => {
               { isNotFound && <NotFoundPage /> }
               { isNotFound && <NotFoundPage /> }
             </div>
             </div>
 
 
-            { !isNotFound && !currentPage?.isEmpty && (
+            { !isNotFound && (
               <div className="grw-side-contents-container">
               <div className="grw-side-contents-container">
                 <div className="grw-side-contents-sticky-container">
                 <div className="grw-side-contents-sticky-container">
 
 

+ 4 - 1
packages/app/src/components/PageAlert/PageAlerts.tsx

@@ -2,6 +2,8 @@ import React from 'react';
 
 
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
+import { useIsNotFound } from '~/stores/context';
+
 import { FixPageGrantAlert } from './FixPageGrantAlert';
 import { FixPageGrantAlert } from './FixPageGrantAlert';
 import { OldRevisionAlert } from './OldRevisionAlert';
 import { OldRevisionAlert } from './OldRevisionAlert';
 import { PageGrantAlert } from './PageGrantAlert';
 import { PageGrantAlert } from './PageGrantAlert';
@@ -12,12 +14,13 @@ const TrashPageAlert = dynamic(() => import('./TrashPageAlert').then(mod => mod.
 
 
 export const PageAlerts = (): JSX.Element => {
 export const PageAlerts = (): JSX.Element => {
 
 
+  const { data: isNotFound } = useIsNotFound();
 
 
   return (
   return (
     <div className="row d-edit-none">
     <div className="row d-edit-none">
       <div className="col-sm-12">
       <div className="col-sm-12">
         {/* alerts */}
         {/* alerts */}
-        <FixPageGrantAlert />
+        { !isNotFound && <FixPageGrantAlert /> }
         <PageGrantAlert />
         <PageGrantAlert />
         <TrashPageAlert />
         <TrashPageAlert />
         <PageStaleAlert />
         <PageStaleAlert />

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

@@ -34,7 +34,7 @@ export const TrashPageAlert = (): JSX.Element => {
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openPutBackPageModal } = usePutBackPageModal();
   const { open: openPutBackPageModal } = usePutBackPageModal();
 
 
-  const lastUpdateUserName = pageData?.lastUpdateUser.name;
+  const lastUpdateUserName = pageData?.lastUpdateUser?.name;
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
   const deletedAt = pageData?.deletedAt ? format(new Date(pageData?.deletedAt), 'yyyy/MM/dd HH:mm') : '';
   const revisionId = pageData?.revision?._id;
   const revisionId = pageData?.revision?._id;
 
 

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

@@ -108,9 +108,9 @@ const PageEditor = (props: Props): JSX.Element => {
 
 
   useEffect(() => {
   useEffect(() => {
     if (currentPage != null) {
     if (currentPage != null) {
-      setMarkdown(currentPage.revision.body);
+      setMarkdown(currentPage.revision?.body);
     }
     }
-  }, [currentPage, currentPage?.revision.body]);
+  }, [currentPage, currentPage?.revision?.body]);
 
 
 
 
   const editorRef = useRef<EditorRef>(null);
   const editorRef = useRef<EditorRef>(null);

+ 5 - 1
packages/app/src/components/PagePathNav.tsx

@@ -3,6 +3,8 @@ import React, { FC } from 'react';
 import { DevidedPagePath } from '@growi/core';
 import { DevidedPagePath } from '@growi/core';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
+import { useIsNotFound } from '~/stores/context';
+
 import LinkedPagePath from '../models/linked-page-path';
 import LinkedPagePath from '../models/linked-page-path';
 
 
 import PagePathHierarchicalLink from './PagePathHierarchicalLink';
 import PagePathHierarchicalLink from './PagePathHierarchicalLink';
@@ -21,6 +23,8 @@ const PagePathNav: FC<Props> = (props: Props) => {
   } = props;
   } = props;
   const dPagePath = new DevidedPagePath(pagePath, false, true);
   const dPagePath = new DevidedPagePath(pagePath, false, true);
 
 
+  const { data: isNotFound } = useIsNotFound();
+
   const CopyDropdown = dynamic(() => import('./Page/CopyDropdown'), { ssr: false });
   const CopyDropdown = dynamic(() => import('./Page/CopyDropdown'), { ssr: false });
 
 
   let formerLink;
   let formerLink;
@@ -47,7 +51,7 @@ const PagePathNav: FC<Props> = (props: Props) => {
       {formerLink}
       {formerLink}
       <span className="d-flex align-items-center">
       <span className="d-flex align-items-center">
         <h1 className="m-0">{latterLink}</h1>
         <h1 className="m-0">{latterLink}</h1>
-        { pageId != null && (
+        { pageId != null && !isNotFound && (
           <div className="mx-2">
           <div className="mx-2">
             <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName={copyDropdownToggleClassName}>
             <CopyDropdown pageId={pageId} pagePath={pagePath} dropdownToggleId={copyDropdownId} dropdownToggleClassName={copyDropdownToggleClassName}>
               <i className="ti ti-clipboard"></i>
               <i className="ti ti-clipboard"></i>

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

@@ -245,7 +245,7 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   useIsNotCreatable(props.isForbidden || !isCreatablePage(pageWithMeta?.data.path ?? '')); // TODO: need to include props.isIdentical
   useIsNotCreatable(props.isForbidden || !isCreatablePage(pageWithMeta?.data.path ?? '')); // TODO: need to include props.isIdentical
   useCurrentPagePath(pageWithMeta?.data.path);
   useCurrentPagePath(pageWithMeta?.data.path);
   useCurrentPathname(props.currentPathname);
   useCurrentPathname(props.currentPathname);
-  useEditingMarkdown(pageWithMeta?.data.revision.body);
+  useEditingMarkdown(pageWithMeta?.data.revision?.body);
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { data: grantData } = useSWRxIsGrantNormalized(pageId);
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
   const { mutate: mutateSelectedGrant } = useSelectedGrant();
 
 
@@ -256,7 +256,8 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
 
 
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   // sync pathname by Shallow Routing https://nextjs.org/docs/routing/shallow-routing
   useEffect(() => {
   useEffect(() => {
-    if (isClient() && window.location.pathname !== props.currentPathname) {
+    const decodedURI = decodeURI(window.location.pathname);
+    if (isClient() && decodedURI !== props.currentPathname) {
       router.replace(props.currentPathname, undefined, { shallow: true });
       router.replace(props.currentPathname, undefined, { shallow: true });
     }
     }
   }, [props.currentPathname, router]);
   }, [props.currentPathname, router]);
@@ -432,6 +433,8 @@ async function injectRoutingInformation(context: GetServerSidePropsContext, prop
     props.isForbidden = count > 0;
     props.isForbidden = count > 0;
   }
   }
   else {
   else {
+    props.isNotFound = page.isEmpty;
+
     // /62a88db47fed8b2d94f30000 ==> /path/to/page
     // /62a88db47fed8b2d94f30000 ==> /path/to/page
     if (isPermalink && page.isEmpty) {
     if (isPermalink && page.isEmpty) {
       props.currentPathname = page.path;
       props.currentPathname = page.path;

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

@@ -443,6 +443,7 @@ module.exports = (crowi) => {
     const page = await Page.findByIdAndViewer(pageId, req.user, null, false);
     const page = await Page.findByIdAndViewer(pageId, req.user, null, false);
 
 
     if (page == null) {
     if (page == null) {
+      // Empty page should not be related to grant API
       return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
       return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
     }
     }
 
 
@@ -519,6 +520,7 @@ module.exports = (crowi) => {
     const page = await Page.findByIdAndViewer(pageId, req.user, null);
     const page = await Page.findByIdAndViewer(pageId, req.user, null);
 
 
     if (page == null) {
     if (page == null) {
+      // Empty page should not be related to grant API
       return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
       return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
     }
     }
 
 
@@ -543,6 +545,7 @@ module.exports = (crowi) => {
     const page = await Page.findByIdAndViewer(pageId, req.user, null, false);
     const page = await Page.findByIdAndViewer(pageId, req.user, null, false);
 
 
     if (page == null) {
     if (page == null) {
+      // Empty page should not be related to grant API
       return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
       return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
     }
     }
 
 

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

@@ -27,7 +27,7 @@ const addCustomFunctionToResponse = (express, crowi) => {
         return new ErrorV3(e.message, null, e.stack);
         return new ErrorV3(e.message, null, e.stack);
       }
       }
       if (typeof e === 'string') {
       if (typeof e === 'string') {
-        return { message: e };
+        return { message: e, status };
       }
       }
 
 
       throw new Error('invalid error supplied to res.apiv3Err');
       throw new Error('invalid error supplied to res.apiv3Err');

+ 1 - 1
packages/app/src/server/service/page.ts

@@ -227,7 +227,7 @@ class PageService {
       page = await Page.findByIdAndViewer(pageId, user, null, includeEmpty);
       page = await Page.findByIdAndViewer(pageId, user, null, includeEmpty);
     }
     }
     else {
     else {
-      page = await Page.findByPathAndViewer(path, user, null, includeEmpty);
+      page = await Page.findByPathAndViewer(path, user, null, true, includeEmpty);
     }
     }
 
 
     if (page == null) {
     if (page == null) {

+ 14 - 4
packages/app/src/stores/page.tsx

@@ -14,10 +14,20 @@ import { IPageTagsInfo } from '../interfaces/tag';
 
 
 import { useCurrentPageId } from './context';
 import { useCurrentPageId } from './context';
 
 
-export const useSWRxPage = (pageId?: string|null, shareLinkId?: string): SWRResponse<IPagePopulatedToShowRevision, Error> => {
-  return useSWR<IPagePopulatedToShowRevision, Error>(
+export const useSWRxPage = (pageId?: string|null, shareLinkId?: string): SWRResponse<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 }).then(result => result.data.page),
+    (endpoint, pageId, shareLinkId) => apiv3Get<{ page: IPagePopulatedToShowRevision }>(endpoint, { pageId, shareLinkId })
+      .then(result => result.data.page)
+      .catch((errs) => {
+        if (!Array.isArray(errs)) { throw Error('error is not array') }
+        const statusCode = errs[0].status;
+        if (statusCode === 403 || statusCode === 404) {
+          // for NotFoundPage
+          return null;
+        }
+        throw Error('failed to get page');
+      }),
   );
   );
 };
 };
 
 
@@ -28,7 +38,7 @@ export const useSWRxPageByPath = (path?: string): SWRResponse<IPagePopulatedToSh
   );
   );
 };
 };
 
 
-export const useSWRxCurrentPage = (shareLinkId?: string, initialData?: IPagePopulatedToShowRevision): SWRResponse<IPagePopulatedToShowRevision, Error> => {
+export const useSWRxCurrentPage = (shareLinkId?: string, initialData?: IPagePopulatedToShowRevision): SWRResponse<IPagePopulatedToShowRevision|null, Error> => {
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
 
 
   const swrResult = useSWRxPage(currentPageId, shareLinkId);
   const swrResult = useSWRxPage(currentPageId, shareLinkId);

+ 11 - 8
packages/app/src/stores/ui.tsx

@@ -409,19 +409,21 @@ export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> =>
   const { data: currentPageId } = useCurrentPageId();
   const { data: currentPageId } = useCurrentPageId();
   const { data: isTrashPage } = useIsTrashPage();
   const { data: isTrashPage } = useIsTrashPage();
   const { data: isSharedUser } = useIsSharedUser();
   const { data: isSharedUser } = useIsSharedUser();
+  const { data: isNotFound } = useIsNotFound();
 
 
   const pageId = currentPageId;
   const pageId = currentPageId;
-  const includesUndefined = [pageId, isTrashPage, isSharedUser].some(v => v === undefined);
-  const isPageExist = pageId != null;
+  const includesUndefined = [pageId, isTrashPage, isSharedUser, isNotFound].some(v => v === undefined);
+  const isPageExist = (pageId != null) && !isNotFound;
 
 
   return useSWRImmutable(
   return useSWRImmutable(
-    includesUndefined ? null : key,
+    includesUndefined ? null : [key, pageId],
     () => isPageExist && !isTrashPage && !isSharedUser,
     () => isPageExist && !isTrashPage && !isSharedUser,
   );
   );
 };
 };
 
 
 export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
 export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
   const key = 'isAbleToShowTagLabel';
   const key = 'isAbleToShowTagLabel';
+  const { data: pageId } = useCurrentPageId();
   const { data: isUserPage } = useIsUserPage();
   const { data: isUserPage } = useIsUserPage();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: currentPagePath } = useCurrentPagePath();
   const { data: isIdenticalPath } = useIsIdenticalPath();
   const { data: isIdenticalPath } = useIsIdenticalPath();
@@ -434,7 +436,7 @@ export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
   const isViewMode = editorMode === EditorMode.View;
   const isViewMode = editorMode === EditorMode.View;
 
 
   return useSWRImmutable(
   return useSWRImmutable(
-    includesUndefined ? null : [key, editorMode],
+    includesUndefined ? null : [key, editorMode, pageId],
     // "/trash" page does not exist on page collection and unable to add tags
     // "/trash" page does not exist on page collection and unable to add tags
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
     () => !isUserPage && !isTrashTopPage(currentPagePath!) && shareLinkId == null && !isIdenticalPath && !(isViewMode && isNotFound),
     () => !isUserPage && !isTrashTopPage(currentPagePath!) && shareLinkId == null && !isIdenticalPath && !(isViewMode && isNotFound),
@@ -458,14 +460,15 @@ export const useIsAbleToShowPageEditorModeManager = (): SWRResponse<boolean, Err
 
 
 export const useIsAbleToShowPageAuthors = (): SWRResponse<boolean, Error> => {
 export const useIsAbleToShowPageAuthors = (): SWRResponse<boolean, Error> => {
   const key = 'isAbleToShowPageAuthors';
   const key = 'isAbleToShowPageAuthors';
-  const { data: currentPageId } = useCurrentPageId();
+  const { data: pageId } = useCurrentPageId();
   const { data: isUserPage } = useIsUserPage();
   const { data: isUserPage } = useIsUserPage();
+  const { data: isNotFound } = useIsNotFound();
 
 
-  const includesUndefined = [currentPageId, isUserPage].some(v => v === undefined);
-  const isPageExist = currentPageId != null;
+  const includesUndefined = [pageId, isUserPage, isNotFound].some(v => v === undefined);
+  const isPageExist = (pageId != null) && !isNotFound;
 
 
   return useSWRImmutable(
   return useSWRImmutable(
-    includesUndefined ? null : key,
+    includesUndefined ? null : [key, pageId],
     () => isPageExist && !isUserPage,
     () => isPageExist && !isUserPage,
   );
   );
 };
 };