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

Merge branch 'dev/5.0.x' into imprv/pt-search-show-snippet

Taichi Masuyama 4 лет назад
Родитель
Сommit
dd6d102202

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

@@ -4,7 +4,7 @@ import { pagePathUtils } from '@growi/core';
 import {
 import {
   useCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd, useIsAbleToDeleteCompletely,
   useCreatedAt, useDeleteUsername, useDeletedAt, useHasChildren, useHasDraftOnHackmd, useIsAbleToDeleteCompletely,
   useIsDeletable, useIsDeleted, useIsNotCreatable, useIsPageExist, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
   useIsDeletable, useIsDeleted, useIsNotCreatable, useIsPageExist, useIsTrashPage, useIsUserPage, useLastUpdateUsername,
-  usePageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
+  useCurrentPageId, usePageIdOnHackmd, usePageUser, useCurrentPagePath, useRevisionCreatedAt, useRevisionId, useRevisionIdHackmdSynced,
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
   useShareLinkId, useShareLinksNumber, useTemplateTagData, useUpdatedAt, useCreator, useRevisionAuthor, useCurrentUser, useTargetAndAncestors,
 } from '../../stores/context';
 } from '../../stores/context';
 import {
 import {
@@ -75,7 +75,7 @@ const ContextExtractorOnce: FC = () => {
   useIsTrashPage(isTrashPage);
   useIsTrashPage(isTrashPage);
   useIsUserPage(isUserPage);
   useIsUserPage(isUserPage);
   useLastUpdateUsername(lastUpdateUsername);
   useLastUpdateUsername(lastUpdateUsername);
-  usePageId(pageId);
+  useCurrentPageId(pageId);
   usePageIdOnHackmd(pageIdOnHackmd);
   usePageIdOnHackmd(pageIdOnHackmd);
   usePageUser(pageUser);
   usePageUser(pageUser);
   useCurrentPagePath(path);
   useCurrentPagePath(path);

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

@@ -149,7 +149,7 @@ const PageDeleteModal: FC<Props> = (props: Props) => {
         { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
         { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
-        <div className="form-group">
+        <div className="form-group grw-scrollable-modal-body pb-1">
           <label>{ t('modal_delete.deleting_page') }:</label><br />
           <label>{ t('modal_delete.deleting_page') }:</label><br />
           {/* Todo: change the way to show path on modal when too many pages are selected */}
           {/* Todo: change the way to show path on modal when too many pages are selected */}
           {/* https://redmine.weseek.co.jp/issues/82787 */}
           {/* https://redmine.weseek.co.jp/issues/82787 */}

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

@@ -2,6 +2,7 @@ import React, { FC, memo } from 'react';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 
 
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
+import { useCurrentPagePath, useCurrentPageId, useTargetAndAncestors } from '~/stores/context';
 
 
 import ItemsTree from './PageTree/ItemsTree';
 import ItemsTree from './PageTree/ItemsTree';
 import PrivateLegacyPages from './PageTree/PrivateLegacyPages';
 import PrivateLegacyPages from './PageTree/PrivateLegacyPages';
@@ -10,7 +11,13 @@ import PrivateLegacyPages from './PageTree/PrivateLegacyPages';
 const PageTree: FC = memo(() => {
 const PageTree: FC = memo(() => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const { data } = useSWRxV5MigrationStatus();
+  const { data: currentPath } = useCurrentPagePath();
+  const { data: targetId } = useCurrentPageId();
+  const { data: targetAndAncestorsData } = useTargetAndAncestors();
+
+  const { data: migrationStatus } = useSWRxV5MigrationStatus();
+
+  const path = currentPath || '/';
 
 
   return (
   return (
     <>
     <>
@@ -19,12 +26,12 @@ const PageTree: FC = memo(() => {
       </div>
       </div>
 
 
       <div className="grw-sidebar-content-body">
       <div className="grw-sidebar-content-body">
-        <ItemsTree />
+        <ItemsTree targetPath={path} targetId={targetId} targetAndAncestorsData={targetAndAncestorsData} />
       </div>
       </div>
 
 
       <div className="grw-sidebar-content-footer">
       <div className="grw-sidebar-content-footer">
         {
         {
-          data?.migratablePagesCount != null && data.migratablePagesCount !== 0 && (
+          migrationStatus?.migratablePagesCount != null && migrationStatus.migratablePagesCount !== 0 && (
             <PrivateLegacyPages />
             <PrivateLegacyPages />
           )
           )
         }
         }

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

@@ -7,26 +7,28 @@ import { useTranslation } from 'react-i18next';
 import { ItemNode } from './ItemNode';
 import { ItemNode } from './ItemNode';
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
 import { useSWRxPageChildren } from '../../../stores/page-listing';
 import { useSWRxPageChildren } from '../../../stores/page-listing';
-import { usePageId } from '../../../stores/context';
 import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTextInput';
 import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTextInput';
 import PageItemControl from '../../Common/Dropdown/PageItemControl';
 import PageItemControl from '../../Common/Dropdown/PageItemControl';
 
 
 
 
 interface ItemProps {
 interface ItemProps {
   itemNode: ItemNode
   itemNode: ItemNode
+  targetId?: string
   isOpen?: boolean
   isOpen?: boolean
 }
 }
 
 
 // Utility to mark target
 // Utility to mark target
-const markTarget = (children: ItemNode[], targetId: string): void => {
+const markTarget = (children: ItemNode[], targetId?: string): void => {
+  if (targetId == null) {
+    return;
+  }
+
   children.forEach((node) => {
   children.forEach((node) => {
     if (node.page._id === targetId) {
     if (node.page._id === targetId) {
       node.page.isTarget = true;
       node.page.isTarget = true;
     }
     }
     return node;
     return node;
   });
   });
-
-  return;
 };
 };
 
 
 type ItemControlProps = {
 type ItemControlProps = {
@@ -82,7 +84,7 @@ const ItemCount: FC = () => {
 
 
 const Item: FC<ItemProps> = (props: ItemProps) => {
 const Item: FC<ItemProps> = (props: ItemProps) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { itemNode, isOpen: _isOpen = false } = props;
+  const { itemNode, targetId, isOpen: _isOpen = false } = props;
 
 
   const { page, children } = itemNode;
   const { page, children } = itemNode;
 
 
@@ -91,7 +93,6 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
 
   const [isNewPageInputShown, setNewPageInputShown] = useState(false);
   const [isNewPageInputShown, setNewPageInputShown] = useState(false);
 
 
-  const { data: targetId } = usePageId();
   const { data, error } = useSWRxPageChildren(isOpen ? page._id : null);
   const { data, error } = useSWRxPageChildren(isOpen ? page._id : null);
 
 
   const hasChildren = useCallback((): boolean => {
   const hasChildren = useCallback((): boolean => {

+ 33 - 32
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -3,8 +3,9 @@ import React, { FC } from 'react';
 import { IPageHasId } from '../../../interfaces/page';
 import { IPageHasId } from '../../../interfaces/page';
 import { ItemNode } from './ItemNode';
 import { ItemNode } from './ItemNode';
 import Item from './Item';
 import Item from './Item';
-import { useSWRxPageAncestorsChildren } from '../../../stores/page-listing';
-import { useTargetAndAncestors, useCurrentPagePath } from '../../../stores/context';
+import { useSWRxPageAncestorsChildren, useSWRxRootPage } from '../../../stores/page-listing';
+import { TargetAndAncestors } from '~/interfaces/page-listing-results';
+import { toastError } from '~/client/util/apiNotification';
 
 
 
 
 /*
 /*
@@ -41,53 +42,53 @@ const generateInitialNodeAfterResponse = (ancestorsChildren: Record<string, Part
   return rootNode;
   return rootNode;
 };
 };
 
 
+type ItemsTreeProps = {
+  targetPath: string
+  targetId?: string
+  targetAndAncestorsData?: TargetAndAncestors
+}
+
+const renderByInitialNode = (initialNode: ItemNode, targetId?: string): JSX.Element => {
+  return (
+    <div className="grw-pagetree p-3">
+      <Item key={initialNode.page.path} targetId={targetId} itemNode={initialNode} isOpen />
+    </div>
+  );
+};
+
 
 
 /*
 /*
  * ItemsTree
  * ItemsTree
  */
  */
-const ItemsTree: FC = () => {
-  const { data: currentPath } = useCurrentPagePath();
-
-  const { data, error } = useTargetAndAncestors();
-
-  const { data: ancestorsChildrenData, error: error2 } = useSWRxPageAncestorsChildren(currentPath || null);
+const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
+  const { targetPath, targetId, targetAndAncestorsData } = props;
 
 
-  if (error != null || error2 != null) {
-    return null;
-  }
+  const { data: ancestorsChildrenData, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
+  const { data: rootPageData, error: error2 } = useSWRxRootPage();
 
 
-  if (data == null) {
+  if (error1 != null || error2 != null) {
+    // TODO: improve message
+    toastError('Error occurred while fetching pages to render PageTree');
     return null;
     return null;
   }
   }
 
 
-  const { targetAndAncestors, rootPage } = data;
-
-  let initialNode: ItemNode;
-
   /*
   /*
-   * Before swr response comes back
+   * Render completely
    */
    */
-  if (ancestorsChildrenData == null) {
-    initialNode = generateInitialNodeBeforeResponse(targetAndAncestors);
+  if (ancestorsChildrenData != null && rootPageData != null) {
+    const initialNode = generateInitialNodeAfterResponse(ancestorsChildrenData.ancestorsChildren, new ItemNode(rootPageData.rootPage));
+    return renderByInitialNode(initialNode, targetId);
   }
   }
 
 
   /*
   /*
-   * When swr request finishes
+   * Before swr response comes back
    */
    */
-  else {
-    const { ancestorsChildren } = ancestorsChildrenData;
-
-    const rootNode = new ItemNode(rootPage);
-
-    initialNode = generateInitialNodeAfterResponse(ancestorsChildren, rootNode);
+  if (targetAndAncestorsData != null) {
+    const initialNode = generateInitialNodeBeforeResponse(targetAndAncestorsData.targetAndAncestors);
+    return renderByInitialNode(initialNode, targetId);
   }
   }
 
 
-  const isOpen = true;
-  return (
-    <div className="grw-pagetree p-3">
-      <Item key={(initialNode as ItemNode).page.path} itemNode={(initialNode as ItemNode)} isOpen={isOpen} />
-    </div>
-  );
+  return null;
 };
 };
 
 
 
 

+ 6 - 1
packages/app/src/interfaces/page-listing-results.ts

@@ -1,7 +1,12 @@
-import { IPageForItem } from './page';
+import { IPageForItem, IPageHasId } from './page';
 
 
 
 
 type ParentPath = string;
 type ParentPath = string;
+
+export interface RootPageResult {
+  rootPage: IPageHasId
+}
+
 export interface AncestorsChildrenResult {
 export interface AncestorsChildrenResult {
   ancestorsChildren: Record<ParentPath, Partial<IPageForItem>[]>
   ancestorsChildren: Record<ParentPath, Partial<IPageForItem>[]>
 }
 }

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

@@ -41,7 +41,7 @@ export interface PageModel extends Model<PageDocument> {
   [x: string]: any; // for obsolete methods
   [x: string]: any; // for obsolete methods
   createEmptyPagesByPaths(paths: string[]): Promise<void>
   createEmptyPagesByPaths(paths: string[]): Promise<void>
   getParentIdAndFillAncestors(path: string): Promise<string | null>
   getParentIdAndFillAncestors(path: string): Promise<string | null>
-  findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?): Promise<PageDocument[]>
+  findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: boolean): Promise<PageDocument[]>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
   findChildrenByParentPathOrIdAndViewer(parentPathOrId: string, user, userGroups?): Promise<PageDocument[]>
   findChildrenByParentPathOrIdAndViewer(parentPathOrId: string, user, userGroups?): Promise<PageDocument[]>
   findAncestorsChildrenByPathAndViewer(path: string, user, userGroups?): Promise<Record<string, PageDocument[]>>
   findAncestorsChildrenByPathAndViewer(path: string, user, userGroups?): Promise<Record<string, PageDocument[]>>

+ 14 - 0
packages/app/src/server/routes/apiv3/page-listing.ts

@@ -41,6 +41,20 @@ export default (crowi: Crowi): Router => {
   const router = express.Router();
   const router = express.Router();
 
 
 
 
+  router.get('/root', accessTokenParser, loginRequiredStrictly, async(req: AuthorizedRequest, res: ApiV3Response) => {
+    const Page: PageModel = crowi.model('Page');
+
+    let rootPage;
+    try {
+      rootPage = await Page.findByPathAndViewer('/', req.user, null, true);
+    }
+    catch (err) {
+      return res.apiv3Err(new ErrorV3('rootPage not found'));
+    }
+
+    return res.apiv3({ rootPage });
+  });
+
   // eslint-disable-next-line max-len
   // eslint-disable-next-line max-len
   router.get('/ancestors-children', accessTokenParser, loginRequiredStrictly, ...validator.pagePathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
   router.get('/ancestors-children', accessTokenParser, loginRequiredStrictly, ...validator.pagePathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
     const { path } = req.query;
     const { path } = req.query;

+ 4 - 4
packages/app/src/stores/context.tsx

@@ -19,13 +19,13 @@ export const useRevisionId = (initialData?: Nullable<any>): SWRResponse<Nullable
   return useStaticSWR<Nullable<any>, Error>('revisionId', initialData ?? null);
   return useStaticSWR<Nullable<any>, Error>('revisionId', initialData ?? null);
 };
 };
 
 
-export const useCurrentPagePath = (initialData?: Nullable<string>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('currentPagePath', initialData ?? null);
+export const useCurrentPagePath = (initialData?: Nullable<string>): SWRResponse<Nullable<string>, Error> => {
+  return useStaticSWR<Nullable<string>, Error>('currentPagePath', initialData ?? null);
 };
 };
 
 
 
 
-export const usePageId = (initialData?: Nullable<string>): SWRResponse<Nullable<any>, Error> => {
-  return useStaticSWR<Nullable<any>, Error>('pageId', initialData ?? null);
+export const useCurrentPageId = (initialData?: Nullable<string>): SWRResponse<Nullable<any>, Error> => {
+  return useStaticSWR<Nullable<any>, Error>('currentPageId', initialData ?? null);
 };
 };
 
 
 export const useRevisionCreatedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {
 export const useRevisionCreatedAt = (initialData?: Nullable<any>): SWRResponse<Nullable<any>, Error> => {

+ 15 - 1
packages/app/src/stores/page-listing.tsx

@@ -1,9 +1,23 @@
 import useSWR, { SWRResponse } from 'swr';
 import useSWR, { SWRResponse } from 'swr';
 
 
 import { apiv3Get } from '../client/util/apiv3-client';
 import { apiv3Get } from '../client/util/apiv3-client';
-import { AncestorsChildrenResult, ChildrenResult, V5MigrationStatus } from '../interfaces/page-listing-results';
+import {
+  AncestorsChildrenResult, ChildrenResult, V5MigrationStatus, RootPageResult,
+} from '../interfaces/page-listing-results';
 
 
 
 
+export const useSWRxRootPage = (): SWRResponse<RootPageResult, Error> => {
+  return useSWR(
+    '/page-listing/root',
+    endpoint => apiv3Get(endpoint).then((response) => {
+      return {
+        rootPage: response.data.rootPage,
+      };
+    }),
+    { revalidateOnFocus: false },
+  );
+};
+
 export const useSWRxPageAncestorsChildren = (
 export const useSWRxPageAncestorsChildren = (
     path: string | null,
     path: string | null,
 ): SWRResponse<AncestorsChildrenResult, Error> => {
 ): SWRResponse<AncestorsChildrenResult, Error> => {

+ 5 - 0
packages/app/src/styles/_layout.scss

@@ -31,6 +31,11 @@ body.growi-layout-fluid .grw-container-convertible {
   border-bottom: 1px solid transparent;
   border-bottom: 1px solid transparent;
 }
 }
 
 
+.grw-scrollable-modal-body {
+  max-height: calc(100vh - 330px);
+  overflow-y: scroll;
+}
+
 // padding settings for GrowiNavbarBottom
 // padding settings for GrowiNavbarBottom
 .page-wrapper {
 .page-wrapper {
   padding-bottom: $grw-navbar-bottom-height;
   padding-bottom: $grw-navbar-bottom-height;