Taichi Masuyama před 4 roky
rodič
revize
287395cb94

+ 1 - 0
packages/app/src/client/services/PageContainer.js

@@ -82,6 +82,7 @@ export default class PageContainer extends Container {
       templateTagData: mainContent.getAttribute('data-template-tags') || null,
       shareLinksNumber: mainContent.getAttribute('data-share-links-number'),
       shareLinkId: JSON.parse(mainContent.getAttribute('data-share-link-id') || null),
+      targetAndAncestors: JSON.parse(mainContent.getAttribute('data-target-and-ancestors') || null),
 
       // latest(on remote) information
       remoteRevisionId: revisionId,

+ 12 - 15
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -1,9 +1,10 @@
-import React, { FC, useState } from 'react';
+import React, { FC } from 'react';
 
 import { IPage } from '../../../interfaces/page';
 import { ItemNode } from './ItemNode';
 import Item from './Item';
-import { useSWRxPageAncestors, useSWRxPageAncestorsChildren } from '../../../stores/page-listing';
+import { useSWRxPageAncestorsChildren } from '../../../stores/page-listing';
+import { useTargetAndAncestors } from '../../../stores/context';
 
 /*
  * Utility to generate initial node and return
@@ -31,22 +32,22 @@ const generateInitialNode = (targetAndAncestors: Partial<IPage>[]): ItemNode =>
 const ItemsTree: FC = () => {
   // TODO: get from props
   const path = '/Sandbox/Bootstrap4';
-  const id = '6181188ae38676152e464fc2';
-
-  const [initialNode, setInitialNode] = useState<ItemNode | null>(null);
 
   // initial request
-  const { data: ancestorsData, error } = useSWRxPageAncestors(path, id);
+  const { data: targetAndAncestors, error } = useTargetAndAncestors();
 
   // secondary request
-  const { data: ancestorsChildrenData, error: error2 } = useSWRxPageAncestorsChildren(ancestorsData != null ? path : null);
+  const { data: ancestorsChildrenData, error: error2 } = useSWRxPageAncestorsChildren(targetAndAncestors != null ? path : null);
+
+  if (error != null || error2 != null) {
+    return null;
+  }
 
-  if (error != null || ancestorsData == null) {
+  if (targetAndAncestors == null) {
     return null;
   }
 
-  const { targetAndAncestors } = ancestorsData;
-  const newInitialNode = generateInitialNode(targetAndAncestors);
+  const initialNode = generateInitialNode(targetAndAncestors);
 
   /*
    * when second SWR resolved
@@ -57,7 +58,7 @@ const ItemsTree: FC = () => {
 
     // flatten ancestors
     const partialChildren: ItemNode[] = [];
-    let currentNode = newInitialNode;
+    let currentNode = initialNode;
     while (currentNode.hasChildren() && currentNode?.children?.[0] != null) {
       const child = currentNode.children[0];
       partialChildren.push(child);
@@ -71,10 +72,6 @@ const ItemsTree: FC = () => {
     });
   }
 
-  setInitialNode(newInitialNode); // rerender
-
-  if (initialNode == null) return null;
-
   const isOpen = true;
   return (
     <>

+ 7 - 0
packages/app/src/components/Sidebar/SidebarContents.jsx

@@ -5,6 +5,7 @@ import { withTranslation } from 'react-i18next';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import NavigationContainer from '~/client/services/NavigationContainer';
+import { useTargetAndAncestors } from '../../stores/context';
 
 import RecentChanges from './RecentChanges';
 import CustomSidebar from './CustomSidebar';
@@ -13,6 +14,12 @@ import PageTree from './PageTree';
 const SidebarContents = (props) => {
   const { navigationContainer, isSharedUser } = props;
 
+  const pageContainer = navigationContainer.getPageContainer();
+
+  const { targetAndAncestors } = pageContainer.state;
+
+  useTargetAndAncestors(targetAndAncestors);
+
   if (isSharedUser) {
     return null;
   }

+ 3 - 0
packages/app/src/interfaces/has-object-id.ts

@@ -0,0 +1,3 @@
+export interface HasObjectId {
+  _id: string,
+}

+ 3 - 5
packages/app/src/interfaces/page-listing-results.ts

@@ -1,12 +1,10 @@
 import { IPage } from './page';
+import { HasObjectId } from './has-object-id';
 
 
 type ParentPath = string;
 export interface AncestorsChildrenResult {
-  ancestorsChildren: Record<ParentPath, Partial<IPage>[]>
+  ancestorsChildren: Record<ParentPath, Partial<IPage & HasObjectId>[]>
 }
 
-
-export interface TargetAndAncestorsResult {
-  targetAndAncestors: Partial<IPage>[]
-}
+export type TargetAndAncestors = Partial<IPage & HasObjectId>[];

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

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

+ 6 - 0
packages/app/src/server/models/obsolete-page.js

@@ -228,6 +228,12 @@ export class PageQueryBuilder {
     return this;
   }
 
+  addConditionToMinimizeDataForRendering() {
+    this.query = this.query.select('_id path isEmpty grant');
+
+    return this;
+  }
+
   addConditionToListByPathsArray(paths) {
     this.query = this.query
       .and({

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

@@ -31,7 +31,7 @@ const PAGE_GRANT_ERROR = 1;
 const STATUS_PUBLISHED = 'published';
 const STATUS_DELETED = 'deleted';
 
-export interface PageDocument extends Omit<IPage, '_id'>, Document {}
+export interface PageDocument extends IPage, Document {}
 
 export interface PageModel extends Model<PageDocument> {
   createEmptyPagesByPaths(paths: string[]): Promise<void>
@@ -246,7 +246,7 @@ schema.statics.findTargetAndAncestorsByPathOrId = async function(pathOrId: strin
   if (!hasSlash(pathOrId)) {
     const _id = pathOrId;
     const page = await this.findOne({ _id });
-    if (page == null) throw Error('Page not found.');
+    if (page == null) throw new Error('Page not found.');
 
     path = page.path;
   }
@@ -261,6 +261,7 @@ schema.statics.findTargetAndAncestorsByPathOrId = async function(pathOrId: strin
   const queryBuilder = new PageQueryBuilder(this.find());
   const _ancestors: PageDocument[] = await queryBuilder
     .addConditionToListByPathsArray(ancestorPaths)
+    .addConditionToMinimizeDataForRendering()
     .addConditionToSortAncestorPages()
     .query
     .lean()

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

@@ -1,5 +1,4 @@
 import express, { Request, Router } from 'express';
-import { pagePathUtils } from '@growi/core';
 import { query, oneOf } from 'express-validator';
 
 import { PageDocument, PageModel } from '../../models/page';
@@ -8,8 +7,6 @@ import loggerFactory from '../../../utils/logger';
 import Crowi from '../../crowi';
 import { ApiV3Response } from './interfaces/apiv3-response';
 
-const { isTopPage } = pagePathUtils;
-
 const logger = loggerFactory('growi:routes:apiv3:page-tree');
 
 /*
@@ -55,31 +52,6 @@ export default (crowi: Crowi): Router => {
     return res.apiv3({ ancestorsChildren });
   });
 
-  /*
-   * In most cases, using path should be prioritized
-   */
-  // eslint-disable-next-line max-len
-  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 targetAndAncestors: PageDocument[];
-    try {
-      targetAndAncestors = await Page.findTargetAndAncestorsByPathOrId((path || id) as string);
-
-      if (targetAndAncestors.length === 0 && !isTopPage(path as string)) {
-        throw Error('Ancestors must have at least one page.');
-      }
-    }
-    catch (err) {
-      logger.error('Error occurred while finding pages.', err);
-      return res.apiv3Err(new ErrorV3('Error occurred while finding pages.'));
-    }
-
-    return res.apiv3({ targetAndAncestors });
-  });
-
   /*
    * In most cases, using id should be prioritized
    */

+ 15 - 1
packages/app/src/server/routes/page.js

@@ -4,7 +4,7 @@ import loggerFactory from '~/utils/logger';
 
 import UpdatePost from '../models/update-post';
 
-const { isCreatablePage } = pagePathUtils;
+const { isCreatablePage, isTopPage } = pagePathUtils;
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
 const { serializeUserSecurely } = require('../models/serializers/user-serializer');
@@ -264,6 +264,16 @@ module.exports = function(crowi, app) {
     renderVars.pages = result.pages;
   }
 
+  async function addRenderVarsForPageTree(renderVars, path) {
+    const targetAndAncestors = await Page.findTargetAndAncestorsByPathOrId(path);
+
+    if (targetAndAncestors.length === 0 && !isTopPage(path)) {
+      throw new Error('Ancestors must have at least one page.');
+    }
+
+    renderVars.targetAndAncestors = targetAndAncestors;
+  }
+
   function replacePlaceholdersOfTemplate(template, req) {
     if (req.user == null) {
       return '';
@@ -329,6 +339,8 @@ module.exports = function(crowi, app) {
 
     await addRenderVarsForDescendants(renderVars, portalPath, req.user, offset, limit);
 
+    await addRenderVarsForPageTree(renderVars, portalPath);
+
     await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     return res.render(view, renderVars);
   }
@@ -383,6 +395,8 @@ module.exports = function(crowi, app) {
       await addRenderVarsForUserPage(renderVars, page);
     }
 
+    await addRenderVarsForPageTree(renderVars, path);
+
     await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     return res.render(view, renderVars);
   }

+ 1 - 0
packages/app/src/server/views/widget/page_content.html

@@ -27,6 +27,7 @@
   data-page-user="{% if pageUser %}{{ pageUser|json }}{% else %}null{% endif %}"
   data-share-links-number="{% if page %}{{ sharelinksNumber }}{% endif %}"
   data-share-link-id="{% if sharelink %}{{ sharelink._id|json }}{% endif %}"
+  data-target-and-ancestors="{% if targetAndAncestors %}{{ targetAndAncestors|json }}{% endif %}"
   >
 {% else %}
 <div id="content-main" class="content-main d-flex"

+ 8 - 0
packages/app/src/stores/context.tsx

@@ -0,0 +1,8 @@
+import { SWRResponse } from 'swr';
+import { useStaticSWR } from './use-static-swr';
+
+import { TargetAndAncestors } from '../interfaces/page-listing-results';
+
+export const useTargetAndAncestors = (initialData?: TargetAndAncestors): SWRResponse<TargetAndAncestors, Error> => {
+  return useStaticSWR<TargetAndAncestors, Error>('targetAndAncestors', initialData);
+};

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

@@ -1,7 +1,7 @@
 import useSWR, { SWRResponse } from 'swr';
 
 import { apiv3Get } from '../client/util/apiv3-client';
-import { TargetAndAncestorsResult, AncestorsChildrenResult } from '../interfaces/page-listing-results';
+import { AncestorsChildrenResult } from '../interfaces/page-listing-results';
 
 
 export const useSWRxPageAncestorsChildren = (
@@ -16,17 +16,3 @@ export const useSWRxPageAncestorsChildren = (
     }),
   );
 };
-
-export const useSWRxPageAncestors = (
-    path: string,
-    id: string,
-): SWRResponse<TargetAndAncestorsResult, Error> => {
-  return useSWR(
-    `/page-listing/target-ancestors?path=${path}&id=${id}`,
-    endpoint => apiv3Get(endpoint).then((response) => {
-      return {
-        targetAndAncestors: response.data.targetAndAncestors,
-      };
-    }),
-  );
-};

+ 27 - 0
packages/app/src/stores/use-static-swr.tsx

@@ -0,0 +1,27 @@
+import useSWR, {
+  Key, SWRResponse, mutate, useSWRConfig,
+} from 'swr';
+import { Fetcher } from 'swr/dist/types';
+
+
+export const useStaticSWR = <Data, Error>(
+  key: Key,
+  updateData?: Data | Fetcher<Data>,
+  initialData?: Data | Fetcher<Data>,
+): SWRResponse<Data, Error> => {
+  const { cache } = useSWRConfig();
+
+  if (updateData == null) {
+    if (cache.get(key) == null && initialData != null) {
+      mutate(key, initialData, false);
+    }
+  }
+  else {
+    mutate(key, updateData);
+  }
+
+  return useSWR(key, null, {
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+  });
+};