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

Renamed & modified initial node process

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

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

@@ -10,9 +10,7 @@ interface ItemProps {
 const Item = memo<ItemProps>((props: ItemProps) => {
   const { itemNode, isOpen = false } = props;
 
-  const { page, children, isPartialChildren } = itemNode;
-
-  // TODO: fetch children if isPartialChildren
+  const { page, children } = itemNode;
 
   if (page == null) {
     return null;
@@ -31,7 +29,7 @@ const Item = memo<ItemProps>((props: ItemProps) => {
       {
         itemNode.hasChildren() && (children as ItemNode[]).map(node => (
           <Item
-            key={node.page.path}
+            key={node.page._id}
             itemNode={node}
             isOpen={false}
           />

+ 6 - 5
packages/app/src/components/Sidebar/PageTree/ItemNode.ts

@@ -1,6 +1,6 @@
 import { IPage } from '~/interfaces/page';
 
-type IPageForItem = Partial<IPage> & {isTarget?: boolean};
+type IPageForItem = Partial<IPage> & { _id?: string, isTarget?: boolean };
 
 export class ItemNode {
 
@@ -8,16 +8,17 @@ export class ItemNode {
 
   children?: ItemNode[];
 
-  isPartialChildren?: boolean;
-
-  constructor(page: IPageForItem, children: ItemNode[] = [], isPartialChildren = false) {
+  constructor(page: IPageForItem, children: ItemNode[] = []) {
     this.page = page;
     this.children = children;
-    this.isPartialChildren = isPartialChildren;
   }
 
   hasChildren(): boolean {
     return this.children != null && this.children?.length > 0;
   }
 
+  static generateNodesFromPages(pages: IPageForItem[]): ItemNode[] {
+    return pages.map(page => new ItemNode(page));
+  }
+
 }

+ 34 - 17
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -3,23 +3,21 @@ import React, { FC } from 'react';
 import { IPage } from '../../../interfaces/page';
 import { ItemNode } from './ItemNode';
 import Item from './Item';
-import { useSWRxPageSiblings, useSWRxPageAncestors } from '../../../stores/page-listing';
+import { useSWRxPageAncestors, useSWRxPageAncestorsChildren } from '../../../stores/page-listing';
 
 /*
- * Utility to generate node tree and return the root node
+ * Utility to generate initial node and return
  */
-const generateInitialTreeFromAncestors = (ancestors: Partial<IPage>[]): ItemNode => {
-  const rootPage = ancestors[ancestors.length - 1]; // the last item is the root
+const generateInitialNode = (targetAndAncestors: Partial<IPage>[]): ItemNode => {
+  const rootPage = targetAndAncestors[targetAndAncestors.length - 1]; // the last item is the root
   if (rootPage?.path !== '/') throw Error('/ not exist in ancestors');
 
-  const ancestorNodes = ancestors.map((page, i): ItemNode => {
-    // isPartialChildren will be false for the target page
-    const isPartialChildren = i !== 0;
-    return new ItemNode(page, [], isPartialChildren);
+  const nodes = targetAndAncestors.map((page): ItemNode => {
+    return new ItemNode(page, []);
   });
 
   // update children for each node
-  const rootNode = ancestorNodes.reduce((child, parent) => {
+  const rootNode = nodes.reduce((child, parent) => {
     parent.children = [child];
     return parent;
   });
@@ -35,26 +33,45 @@ const ItemsTree: FC = () => {
   const path = '/Sandbox/Bootstrap4';
   const id = '6181188ae38676152e464fc2';
 
+  // initial request
   const { data: ancestorsData, error } = useSWRxPageAncestors(path, id);
 
-  if (error != null) {
-    return null;
-  }
+  // secondary request
+  const { data: ancestorsChildrenData, error: error2 } = useSWRxPageAncestorsChildren(path);
 
-  if (ancestorsData == null) {
+  if (error != null || ancestorsData == null) {
     return null;
   }
 
-  const { ancestors } = ancestorsData;
+  const { targetAndAncestors } = ancestorsData;
 
   // create node tree
-  const rootNode = generateInitialTreeFromAncestors(ancestors);
+  const initialNode = generateInitialNode(targetAndAncestors);
 
-  const isOpen = true;
+  if (ancestorsChildrenData != null) {
+    // increment initialNode
+    const { ancestorsChildren } = ancestorsChildrenData;
+
+    // flatten ancestors
+    const partialChildren: ItemNode[] = [];
+    let currentNode = initialNode;
+    while (currentNode.hasChildren() && currentNode?.children?.[0] != null) {
+      const child = currentNode.children[0];
+      partialChildren.push(child);
+      currentNode = child;
+    }
 
+    // update children
+    partialChildren.forEach((node) => {
+      const childPages = ancestorsChildren[node.page.path as string];
+      node.children = ItemNode.generateNodesFromPages(childPages);
+    });
+  }
+
+  const isOpen = true;
   return (
     <>
-      <Item key={rootNode.page.path} itemNode={rootNode} isOpen={isOpen} />
+      <Item key={initialNode.page.path} itemNode={initialNode} isOpen={isOpen} />
     </>
   );
 };

+ 10 - 4
packages/app/src/interfaces/page-listing-results.ts

@@ -1,10 +1,16 @@
 import { IPage } from './page';
 
-export interface SiblingsResult {
-  targetAndSiblings: Partial<IPage>[]
+export interface ChildrenResult {
+  pages: Partial<IPage>[]
 }
 
 
-export interface AncestorsResult {
-  ancestors: Partial<IPage>[]
+type ParentPath = string;
+export interface AncestorsChildrenResult {
+  ancestorsChildren: Record<ParentPath, Partial<IPage>[]>
+}
+
+
+export interface TargetAndAncestorsResult {
+  targetAndAncestors: Partial<IPage>[]
 }

+ 1 - 0
packages/app/src/interfaces/page.ts

@@ -5,6 +5,7 @@ import { ITag } from './tag';
 
 
 export type IPage = {
+  _id?: string,
   path: string,
   status: string,
   revision: Ref<IRevision>,

+ 8 - 3
packages/app/src/server/models/page.ts

@@ -31,15 +31,16 @@ const PAGE_GRANT_ERROR = 1;
 const STATUS_PUBLISHED = 'published';
 const STATUS_DELETED = 'deleted';
 
-export interface PageDocument extends IPage, Document {}
+export interface PageDocument extends Omit<IPage, '_id'>, Document {}
 
 export interface PageModel extends Model<PageDocument> {
   createEmptyPagesByPaths(paths: string[]): Promise<void>
   getParentIdAndFillAncestors(path: string): Promise<string | null>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?): Promise<PageDocument[]>
   findSiblingsByPathAndViewer(path: string | null, user, userGroups?): Promise<PageDocument[]>
-  findAncestorsByPathOrId(pathOrId: string): Promise<PageDocument[]>
+  findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<PageDocument[]>
   findChildrenByParentPathOrIdAndViewer(parentPathOrId: string, user, userGroups?): Promise<PageDocument[]>
+  findAncestorsChildrenByPathAndViewer(path: string, user, userGroups?): Promise<Record<string, PageDocument[]>>
 }
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
@@ -238,8 +239,9 @@ schema.statics.findSiblingsByPathAndViewer = async function(path: string | null,
 
 /*
  * Find all ancestor pages by path. When duplicate pages found, it uses the oldest page as a result
+ * The result will include the target as well
  */
-schema.statics.findAncestorsByPathOrId = async function(pathOrId: string): Promise<PageDocument[]> {
+schema.statics.findTargetAndAncestorsByPathOrId = async function(pathOrId: string): Promise<PageDocument[]> {
   let path;
   if (!hasSlash(pathOrId)) {
     const _id = pathOrId;
@@ -253,6 +255,7 @@ schema.statics.findAncestorsByPathOrId = async function(pathOrId: string): Promi
   }
 
   const ancestorPaths = collectAncestorPaths(path);
+  ancestorPaths.push(path); // include target
 
   // Do not populate
   const queryBuilder = new PageQueryBuilder(this.find());
@@ -290,6 +293,8 @@ schema.statics.findChildrenByParentPathOrIdAndViewer = async function(parentPath
   return queryBuilder.query.lean().exec();
 };
 
+// TODO: implement findAncestorsChildrenByPathAndViewer using lean()
+
 
 /*
  * Merge obsolete page model methods and define new methods which depend on crowi instance

+ 8 - 26
packages/app/src/server/routes/apiv3/page-listing.ts

@@ -45,48 +45,30 @@ export default (crowi: Crowi): Router => {
 
 
   // eslint-disable-next-line max-len
-  router.get('/siblings', 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 Page: PageModel = crowi.model('Page');
 
-    let targetAndSiblings: PageDocument[];
-    try {
-      targetAndSiblings = await Page.findSiblingsByPathAndViewer(path as string, req.user);
-
-      targetAndSiblings = targetAndSiblings.map((page) => {
-        if (page.path === path) {
-          Object.assign(page, { isTarget: true });
-        }
-        return page;
-      });
-    }
-    catch (err) {
-      logger.error('Error occurred while finding pages.', err);
-      return res.apiv3Err(new ErrorV3('Error occurred while finding pages.'));
-    }
-
-    if (isTopPage(path as string)) {
-      targetAndSiblings = targetAndSiblings.filter(page => !isTopPage(page.path));
-    }
+    const ancestorsChildren: Record<string, PageDocument[]> = await Page.findAncestorsChildrenByPathAndViewer(path as string, req.user);
 
-    return res.apiv3({ targetAndSiblings });
+    return res.apiv3({ ancestorsChildren });
   });
 
   /*
    * In most cases, using path should be prioritized
    */
   // eslint-disable-next-line max-len
-  router.get('/ancestors', accessTokenParser, loginRequiredStrictly, validator.pageIdOrPathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
+  router.get('/target-ancestors', accessTokenParser, loginRequiredStrictly, validator.pageIdOrPathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
     const { id, path } = req.query;
 
     const Page: PageModel = crowi.model('Page');
 
-    let ancestors: PageDocument[];
+    let targetAndAncestors: PageDocument[];
     try {
-      ancestors = await Page.findAncestorsByPathOrId((path || id) as string);
+      targetAndAncestors = await Page.findTargetAndAncestorsByPathOrId((path || id) as string);
 
-      if (ancestors.length === 0 && !isTopPage(path as string)) {
+      if (targetAndAncestors.length === 0 && !isTopPage(path as string)) {
         throw Error('Ancestors must have at least one page.');
       }
     }
@@ -95,7 +77,7 @@ export default (crowi: Crowi): Router => {
       return res.apiv3Err(new ErrorV3('Error occurred while finding pages.'));
     }
 
-    return res.apiv3({ ancestors });
+    return res.apiv3({ targetAndAncestors });
   });
 
   /*

+ 22 - 9
packages/app/src/stores/page-listing.tsx

@@ -1,32 +1,45 @@
 import useSWR, { SWRResponse } from 'swr';
 
 import { apiv3Get } from '../client/util/apiv3-client';
-import { SiblingsResult, AncestorsResult } from '../interfaces/page-listing-results';
+import { ChildrenResult, TargetAndAncestorsResult, AncestorsChildrenResult } from '../interfaces/page-listing-results';
 
 
-export const useSWRxPageSiblings = (
+export const useSWRxPageAncestorsChildren = (
     path: string,
-): SWRResponse<SiblingsResult, Error> => {
+): SWRResponse<AncestorsChildrenResult, Error> => {
   return useSWR(
-    `/page-listing/siblings?path=${path}`,
+    `/page-listing/ancestors-children?path=${path}`,
     endpoint => apiv3Get(endpoint).then((response) => {
       return {
-        targetAndSiblings: response.data.targetAndSiblings,
+        ancestorsChildren: response.data.ancestorsChildren,
       };
     }),
   );
 };
 
-
 export const useSWRxPageAncestors = (
     path: string,
     id: string,
-): SWRResponse<AncestorsResult, Error> => {
+): SWRResponse<TargetAndAncestorsResult, Error> => {
+  return useSWR(
+    `/page-listing/target-ancestors?path=${path}&id=${id}`,
+    endpoint => apiv3Get(endpoint).then((response) => {
+      return {
+        targetAndAncestors: response.data.targetAndAncestors,
+      };
+    }),
+  );
+};
+
+
+export const useSWRxPageChildren = (
+    path: string | null,
+): SWRResponse<ChildrenResult, Error> => {
   return useSWR(
-    `/page-listing/ancestors?path=${path}&id=${id}`,
+    path ? `/page-listing/children?path=${path}` : null,
     endpoint => apiv3Get(endpoint).then((response) => {
       return {
-        ancestors: response.data.ancestors,
+        pages: response.data.pages,
       };
     }),
   );