Yuki Takei 3 лет назад
Родитель
Сommit
44c76e328e

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

@@ -45,7 +45,7 @@ export const DescendantsPageListSubstance = (props: SubstanceProps): JSX.Element
   const { data: isGuestUser } = useIsGuestUser();
 
   const pageIds = pagingResult?.items?.map(page => page._id);
-  const { injectTo } = useSWRxPageInfoForList(pageIds, true, true);
+  const { injectTo } = useSWRxPageInfoForList(pageIds, null, true, true);
 
   let pageWithMetas: IDataWithMeta<IPageHasId, IPageInfoForOperation>[] = [];
 

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

@@ -18,7 +18,7 @@ const EmptyTrashButton = () => {
   const { data: pagingResult, mutate } = useSWRxDescendantsPageListForCurrrentPath();
 
   const pageIds = pagingResult?.items?.map(page => page._id);
-  const { injectTo } = useSWRxPageInfoForList(pageIds, true, true);
+  const { injectTo } = useSWRxPageInfoForList(pageIds, null, true, true);
 
   let pageWithMetas: IDataWithMeta<IPageHasId, IPageInfo>[] = [];
 

+ 9 - 19
packages/app/src/components/IdenticalPathPage.tsx

@@ -1,12 +1,12 @@
 import React, { FC } from 'react';
-import { useTranslation } from 'next-i18next';
 
 import { DevidedPagePath } from '@growi/core';
+import { useTranslation } from 'next-i18next';
 
-import { IPageHasId } from '~/interfaces/page';
 import { useCurrentPagePath, useIsSharedUser } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useSWRxPageInfoForList } from '~/stores/page';
+import { useSWRxPagesByPath } from '~/stores/page-listing';
 
 import PageListIcon from './Icons/PageListIcon';
 import { PageListItemL } from './PageList/PageListItemL';
@@ -47,29 +47,21 @@ const IdenticalPathAlert : FC<IdenticalPathAlertProps> = (props: IdenticalPathAl
 };
 
 
-type IdenticalPathPageProps= {
-  // add props and types here
-}
-
-
-const jsonNull = 'null';
-
-const IdenticalPathPage:FC<IdenticalPathPageProps> = (props: IdenticalPathPageProps) => {
+export const IdenticalPathPage = (): JSX.Element => {
   const { t } = useTranslation();
 
-  const identicalPageDocument = document.getElementById('identical-path-page');
-  const pages = JSON.parse(identicalPageDocument?.getAttribute('data-identical-path-pages') || jsonNull) as IPageHasId[];
-
-  const pageIds = pages.map(page => page._id) as string[];
-
-
   const { data: currentPath } = useCurrentPagePath();
   const { data: isSharedUser } = useIsSharedUser();
 
-  const { injectTo } = useSWRxPageInfoForList(pageIds, true, true);
+  const { data: pages } = useSWRxPagesByPath(currentPath);
+  const { injectTo } = useSWRxPageInfoForList(null, currentPath, true, true);
 
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
 
+  if (pages == null) {
+    return <></>;
+  }
+
   const injectedPages = injectTo(pages);
 
   return (
@@ -118,5 +110,3 @@ const IdenticalPathPage:FC<IdenticalPathPageProps> = (props: IdenticalPathPagePr
     </div>
   );
 };
-
-export default IdenticalPathPage;

+ 4 - 2
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -2,7 +2,9 @@ import React, {
   forwardRef,
   ForwardRefRenderFunction, useCallback, useImperativeHandle, useRef,
 } from 'react';
+
 import { useTranslation } from 'next-i18next';
+
 import { ISelectable, ISelectableAll } from '~/client/interfaces/selectable-all';
 import { toastSuccess } from '~/client/util/apiNotification';
 import {
@@ -14,8 +16,8 @@ import { useIsGuestUser } from '~/stores/context';
 import { useSWRxPageInfoForList } from '~/stores/page';
 import { usePageTreeTermManager } from '~/stores/page-listing';
 import { useFullTextSearchTermManager } from '~/stores/search';
-import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 
+import { ForceHideMenuItems } from '../Common/Dropdown/PageItemControl';
 import { PageListItemL } from '../PageList/PageListItemL';
 
 
@@ -41,7 +43,7 @@ const SearchResultListSubstance: ForwardRefRenderFunction<ISelectableAll, Props>
     .map(page => page.data._id);
 
   const { data: isGuestUser } = useIsGuestUser();
-  const { data: idToPageInfo } = useSWRxPageInfoForList(pageIdsWithNoSnippet, true, true);
+  const { data: idToPageInfo } = useSWRxPageInfoForList(pageIdsWithNoSnippet, null, true, true);
 
   // for mutation
   const { advance: advancePt } = usePageTreeTermManager();

+ 69 - 14
packages/app/src/pages/[[...path]].page.tsx

@@ -1,6 +1,7 @@
 import React, { useEffect } from 'react';
 
 import { isClient, pagePathUtils, pathUtils } from '@growi/core';
+import ExtensibleCustomError from 'extensible-custom-error';
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -57,6 +58,13 @@ const logger = loggerFactory('growi:pages:all');
 const { isPermalink: _isPermalink, isUsersHomePage, isTrashPage: _isTrashPage } = pagePathUtils;
 const { removeHeadingSlash } = pathUtils;
 
+
+const IdenticalPathPage = (): JSX.Element => {
+  const IdenticalPathPage = dynamic(() => import('../components/IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
+  return <IdenticalPathPage />;
+};
+
+
 type Props = CommonProps & {
   currentUser: string,
 
@@ -67,6 +75,7 @@ type Props = CommonProps & {
 
   // shareLinkId?: string;
 
+  isIdenticalPathPage?: boolean,
   isForbidden: boolean,
   isNotFound: boolean,
   // isAbleToDeleteCompletely: boolean,
@@ -216,14 +225,24 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
         <div id="main" className={`main ${isUsersHomePage(props.currentPathname) && 'user-page'}`}>
 
           <div className="row">
-            <div className="col grw-page-content-container">
+            <div className="col">
               <div id="content-main" className="content-main grw-container-convertible">
-                {/* <PageAlerts /> */}
-                PageAlerts<br />
-                <DisplaySwitcher />
-                <div id="page-editor-navbar-bottom-container" className="d-none d-edit-block"></div>
-                {/* <PageStatusAlert /> */}
-                PageStatusAlert
+                { props.isIdenticalPathPage && <IdenticalPathPage /> }
+
+                { !props.isIdenticalPathPage && (
+                  <>
+                    {/* <PageAlerts /> */}
+                    PageAlerts<br />
+                    { props.isForbidden
+                      ? <>ForbiddenPage</>
+                      : <DisplaySwitcher />
+                    }
+                    <div id="page-editor-navbar-bottom-container" className="d-none d-edit-block"></div>
+                    {/* <PageStatusAlert /> */}
+                    PageStatusAlert
+                  </>
+                ) }
+
               </div>
             </div>
 
@@ -252,16 +271,38 @@ function getPageIdFromPathname(currentPathname: string): string | null {
   return _isPermalink(currentPathname) ? removeHeadingSlash(currentPathname) : null;
 }
 
+class MultiplePagesHitsError extends ExtensibleCustomError {
+
+  pagePath: string;
+
+  constructor(pagePath: string) {
+    super(`MultiplePagesHitsError occured by '${pagePath}'`);
+    this.pagePath = pagePath;
+  }
+
+}
+
 async function getPageData(context: GetServerSidePropsContext, props: Props): Promise<IPageWithMeta|null> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { revisionId } = req.query;
-  const { pageService } = crowi;
 
-  const { user } = req;
+  const Page = crowi.model('Page') as PageModel;
+  const { pageService } = crowi;
 
   const { currentPathname } = props;
   const pageId = getPageIdFromPathname(currentPathname);
+  const isPermalink = _isPermalink(currentPathname);
+
+  const { user } = req;
+
+  // check whether the specified page path hits to multiple pages
+  if (!isPermalink) {
+    const count = await Page.countByPathAndViewer(currentPathname, user, null, true);
+    if (count > 1) {
+      throw new MultiplePagesHitsError(currentPathname);
+    }
+  }
 
   const result: IPageWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, currentPathname, user, true); // includeEmpty = true, isSharedPage = false
   const page = result?.data as unknown as PageDocument;
@@ -278,7 +319,7 @@ async function getPageData(context: GetServerSidePropsContext, props: Props): Pr
 async function injectRoutingInformation(context: GetServerSidePropsContext, props: Props, pageWithMeta: IPageWithMeta|null): Promise<void> {
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
-  const Page = crowi.model('Page');
+  const Page = crowi.model('Page') as PageModel;
 
   const { currentPathname } = props;
   const pageId = getPageIdFromPathname(currentPathname);
@@ -286,15 +327,17 @@ async function injectRoutingInformation(context: GetServerSidePropsContext, prop
 
   const page = pageWithMeta?.data;
 
-  if (page == null) {
+  if (props.isIdenticalPathPage) {
+    // TBD
+  }
+  else if (page == null) {
     props.isNotFound = true;
 
     // check the page is forbidden or just does not exist.
     const count = isPermalink ? await Page.count({ _id: pageId }) : await Page.count({ path: currentPathname });
     props.isForbidden = count > 0;
   }
-
-  if (page != null) {
+  else {
     // /62a88db47fed8b2d94f30000 ==> /path/to/page
     if (isPermalink && page.isEmpty) {
       props.currentPathname = page.path;
@@ -378,7 +421,19 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   }
 
   const props: Props = result.props as Props;
-  const pageWithMeta = await getPageData(context, props);
+  let pageWithMeta;
+  try {
+    pageWithMeta = await getPageData(context, props);
+    props.pageWithMetaStr = JSON.stringify(pageWithMeta);
+  }
+  catch (err) {
+    if (err instanceof MultiplePagesHitsError) {
+      props.isIdenticalPathPage = true;
+    }
+    else {
+      throw err;
+    }
+  }
 
   injectRoutingInformation(context, props, pageWithMeta);
   injectServerConfigurations(context, props);

+ 14 - 0
packages/app/src/server/models/page.ts

@@ -60,6 +60,7 @@ export interface PageModel extends Model<PageDocument> {
   findByIdsAndViewer(pageIds: ObjectIdLike[], user, userGroups?, includeEmpty?: boolean): Promise<PageDocument[]>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: true, includeEmpty?: boolean): Promise<PageDocument | PageDocument[] | null>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: false, includeEmpty?: boolean): Promise<PageDocument[]>
+  countByPathAndViewer(path: string | null, user, userGroups?, includeEmpty?:boolean): Promise<number>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
   findRecentUpdatedPages(path: string, user, option, includeEmpty?: boolean): Promise<PaginatedPages>
   generateGrantCondition(
@@ -573,6 +574,19 @@ schema.statics.findByPathAndViewer = async function(
   return queryBuilder.query.exec();
 };
 
+schema.statics.countByPathAndViewer = async function(path: string | null, user, userGroups = null, includeEmpty = false): Promise<number> {
+  if (path == null) {
+    throw new Error('path is required.');
+  }
+
+  const baseQuery = this.count({ path });
+  const queryBuilder = new PageQueryBuilder(baseQuery, includeEmpty);
+
+  await queryBuilder.addViewerCondition(user, userGroups);
+
+  return queryBuilder.query.exec();
+};
+
 schema.statics.findRecentUpdatedPages = async function(
     path: string, user, options, includeEmpty = false,
 ): Promise<PaginatedPages> {

+ 10 - 0
packages/app/src/stores/page-listing.tsx

@@ -1,5 +1,8 @@
 import useSWR, { SWRResponse } from 'swr';
 
+import { Nullable } from '~/interfaces/common';
+import { IPageHasId } from '~/interfaces/page';
+
 import { apiv3Get } from '../client/util/apiv3-client';
 import {
   AncestorsChildrenResult, ChildrenResult, V5MigrationStatus, RootPageResult,
@@ -7,6 +10,13 @@ import {
 
 import { ITermNumberManagerUtil, useTermNumberManager } from './use-static-swr';
 
+export const useSWRxPagesByPath = (path?: Nullable<string>): SWRResponse<IPageHasId[], Error> => {
+  const findAll = true;
+  return useSWR<IPageHasId[], Error>(
+    path != null ? ['/page', path, findAll] : null,
+    (endpoint, path, findAll) => apiv3Get(endpoint, { path, findAll }).then(result => result.data.pages),
+  );
+};
 
 export const usePageTreeTermManager = (isDisabled?: boolean) : SWRResponse<number, Error> & ITermNumberManagerUtil => {
   return useTermNumberManager(isDisabled === true ? null : 'fullTextSearchTermNumber');

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

@@ -130,16 +130,19 @@ const isIDataWithMeta = (item: HasObjectId | IDataWithMeta): item is IDataWithMe
 
 export const useSWRxPageInfoForList = (
     pageIds: string[] | null | undefined,
+    path: string | null | undefined = null,
     attachBookmarkCount = false,
     attachShortBody = false,
 ): SWRResponse<Record<string, IPageInfoForListing>, Error> & PageInfoInjector => {
 
-  const shouldFetch = pageIds != null && pageIds.length > 0;
+  const shouldFetch = (pageIds != null && pageIds.length > 0) || path != null;
 
   const swrResult = useSWRImmutable<Record<string, IPageInfoForListing>>(
-    shouldFetch ? ['/page-listing/info', pageIds, attachBookmarkCount, attachShortBody] : null,
-    (endpoint, pageIds, attachBookmarkCount, attachShortBody) => {
-      return apiv3Get(endpoint, { pageIds, attachBookmarkCount, attachShortBody }).then(response => response.data);
+    shouldFetch ? ['/page-listing/info', pageIds, path, attachBookmarkCount, attachShortBody] : null,
+    (endpoint, pageIds, path, attachBookmarkCount, attachShortBody) => {
+      return apiv3Get(endpoint, {
+        pageIds, path, attachBookmarkCount, attachShortBody,
+      }).then(response => response.data);
     },
   );