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

Merge branch 'dev/5.0.x' into
imprv/87696-show-duplilcate-modal-when-clicking-page-item-control

kaori 4 лет назад
Родитель
Сommit
184888e1e3

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

@@ -1,4 +1,4 @@
-import React, { FC, memo, useState } from 'react';
+import React, { FC, memo } from 'react';
 import { useTranslation } from 'react-i18next';
 
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
@@ -8,8 +8,6 @@ import {
 
 import ItemsTree from './PageTree/ItemsTree';
 import PrivateLegacyPages from './PageTree/PrivateLegacyPages';
-import { IPageForPageDeleteModal } from '~/stores/ui';
-
 
 const PageTree: FC = memo(() => {
   const { t } = useTranslation();
@@ -19,13 +17,8 @@ const PageTree: FC = memo(() => {
   const { data: targetId } = useCurrentPageId();
   const { data: targetAndAncestorsData } = useTargetAndAncestors();
   const { data: notFoundTargetPathOrIdData } = useNotFoundTargetPathOrId();
-
   const { data: migrationStatus } = useSWRxV5MigrationStatus();
 
-  // for delete modal
-  const [isDeleteModalOpen, setDeleteModalOpen] = useState(false);
-  const [pagesToDelete, setPagesToDelete] = useState<IPageForPageDeleteModal[]>([]);
-
   const targetPathOrId = targetId || notFoundTargetPathOrIdData?.notFoundTargetPathOrId;
 
   if (migrationStatus == null) {
@@ -56,6 +49,7 @@ const PageTree: FC = memo(() => {
       </>
     );
   }
+
   /*
    * dependencies
    */
@@ -63,15 +57,6 @@ const PageTree: FC = memo(() => {
     return null;
   }
 
-  const onClickDeleteByPage = (page: IPageForPageDeleteModal) => {
-    setDeleteModalOpen(true);
-    setPagesToDelete([page]);
-  };
-
-  const onCloseDelete = () => {
-    setDeleteModalOpen(false);
-  };
-
   const path = currentPath || '/';
 
   return (
@@ -86,12 +71,6 @@ const PageTree: FC = memo(() => {
           targetPath={path}
           targetPathOrId={targetPathOrId}
           targetAndAncestorsData={targetAndAncestorsData}
-          isDeleteModalOpen={isDeleteModalOpen}
-          pagesToDelete={pagesToDelete}
-          isAbleToDeleteCompletely={false} // TODO: pass isAbleToDeleteCompletely
-          isDeleteCompletelyModal={false} // TODO: pass isDeleteCompletelyModal
-          onCloseDelete={onCloseDelete}
-          onClickDeleteByPage={onClickDeleteByPage}
         />
       </div>
 

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

@@ -22,8 +22,8 @@ interface ItemProps {
   itemNode: ItemNode
   targetPathOrId?: string
   isOpen?: boolean
-  onClickDeleteByPage?(page: IPageForPageDeleteModal): void
   onClickDuplicateMenuItem?(pageId: string, path: string): void
+  onClickDeleteByPage?(pageToDelete: IPageForPageDeleteModal | null): void
 }
 
 // Utility to mark target

+ 9 - 30
packages/app/src/components/Sidebar/PageTree/ItemsTree.tsx

@@ -6,8 +6,7 @@ import Item from './Item';
 import { useSWRxPageAncestorsChildren, useSWRxRootPage } from '../../../stores/page-listing';
 import { TargetAndAncestors } from '~/interfaces/page-listing-results';
 import { toastError } from '~/client/util/apiNotification';
-import PageDeleteModal from '~/components/PageDeleteModal';
-import { IPageForPageDeleteModal, usePageDuplicateModalStatus } from '~/stores/ui';
+import { IPageForPageDeleteModal, usePageDuplicateModalStatus, usePageDeleteModalStatus } from '~/stores/ui';
 
 /*
  * Utility to generate initial node
@@ -53,24 +52,14 @@ type ItemsTreeProps = {
   targetPath: string
   targetPathOrId?: string
   targetAndAncestorsData?: TargetAndAncestors
-
-  // for deleteModal
-  isDeleteModalOpen: boolean
-  pagesToDelete: IPageForPageDeleteModal[]
-  isAbleToDeleteCompletely: boolean
-  isDeleteCompletelyModal: boolean
-  onCloseDelete(): void
-  onClickDeleteByPage(page: IPageForPageDeleteModal): void
 }
 
 const renderByInitialNode = (
-    // eslint-disable-next-line max-len
     initialNode: ItemNode,
-    DeleteModal: JSX.Element,
     isEnableActions: boolean,
     targetPathOrId?: string,
     onClickDuplicateMenuItem?: (pageId: string, path: string) => void,
-    onClickDeleteByPage?: (page: IPageForPageDeleteModal) => void,
+    onClickDeleteByPage?: (pageToDelete: IPageForPageDeleteModal | null) => void,
 ): JSX.Element => {
 
   return (
@@ -84,7 +73,6 @@ const renderByInitialNode = (
         onClickDuplicateMenuItem={onClickDuplicateMenuItem}
         onClickDeleteByPage={onClickDeleteByPage}
       />
-      {DeleteModal}
     </ul>
   );
 };
@@ -95,29 +83,21 @@ const renderByInitialNode = (
  */
 const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
   const {
-    targetPath, targetPathOrId, targetAndAncestorsData, isDeleteModalOpen, pagesToDelete, isAbleToDeleteCompletely, isDeleteCompletelyModal,
-    onCloseDelete,
-    onClickDeleteByPage, isEnableActions,
+    targetPath, targetPathOrId, targetAndAncestorsData, isEnableActions,
   } = props;
 
   const { data: ancestorsChildrenData, error: error1 } = useSWRxPageAncestorsChildren(targetPath);
   const { data: rootPageData, error: error2 } = useSWRxRootPage();
   const { open: openDuplicateModal } = usePageDuplicateModalStatus();
+  const { open: openDeleteModal } = usePageDeleteModalStatus();
 
   const onClickDuplicateMenuItem = (pageId: string, path: string) => {
     openDuplicateModal(pageId, path);
   };
 
-  // TODO: show PageDeleteModal with usePageDeleteModalStatus by 87568
-  const DeleteModal = (
-    <PageDeleteModal
-      isOpen={isDeleteModalOpen}
-      // pages={pagesToDelete}
-      isAbleToDeleteCompletely={isAbleToDeleteCompletely}
-      isDeleteCompletelyModal={isDeleteCompletelyModal}
-      onClose={onCloseDelete}
-    />
-  );
+  const onClickDeleteByPage = (pageToDelete: IPageForPageDeleteModal) => {
+    openDeleteModal([pageToDelete]);
+  };
 
   if (error1 != null || error2 != null) {
     // TODO: improve message
@@ -130,7 +110,7 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
    */
   if (ancestorsChildrenData != null && rootPageData != null) {
     const initialNode = generateInitialNodeAfterResponse(ancestorsChildrenData.ancestorsChildren, new ItemNode(rootPageData.rootPage));
-    return renderByInitialNode(initialNode, DeleteModal, isEnableActions, targetPathOrId, onClickDuplicateMenuItem, onClickDeleteByPage);
+    return renderByInitialNode(initialNode, isEnableActions, targetPathOrId, onClickDuplicateMenuItem, onClickDeleteByPage);
   }
 
   /*
@@ -138,11 +118,10 @@ const ItemsTree: FC<ItemsTreeProps> = (props: ItemsTreeProps) => {
    */
   if (targetAndAncestorsData != null) {
     const initialNode = generateInitialNodeBeforeResponse(targetAndAncestorsData.targetAndAncestors);
-    return renderByInitialNode(initialNode, DeleteModal, isEnableActions, targetPathOrId, onClickDuplicateMenuItem, onClickDeleteByPage);
+    return renderByInitialNode(initialNode, isEnableActions, targetPathOrId, onClickDuplicateMenuItem, onClickDeleteByPage);
   }
 
   return null;
 };
 
-
 export default ItemsTree;

+ 28 - 16
packages/app/src/server/models/page.ts

@@ -145,7 +145,7 @@ schema.statics.createEmptyPagesByPaths = async function(paths: string[], publicO
 };
 
 schema.statics.createEmptyPage = async function(
-    path: string, parent: any, // TODO: improve type including IPage at https://redmine.weseek.co.jp/issues/86506
+    path: string, parent: any, descendantCount: number, // TODO: improve type including IPage at https://redmine.weseek.co.jp/issues/86506
 ): Promise<PageDocument & { _id: any }> {
   if (parent == null) {
     throw Error('parent must not be null');
@@ -156,6 +156,7 @@ schema.statics.createEmptyPage = async function(
   page.path = path;
   page.isEmpty = true;
   page.parent = parent;
+  page.descendantCount = descendantCount;
 
   return page.save();
 };
@@ -174,7 +175,7 @@ schema.statics.replaceTargetWithPage = async function(exPage, pageToReplaceWith?
   }
 
   // create empty page at path
-  const newTarget = pageToReplaceWith == null ? await this.createEmptyPage(exPage.path, parent) : pageToReplaceWith;
+  const newTarget = pageToReplaceWith == null ? await this.createEmptyPage(exPage.path, parent, exPage.descendantCount) : pageToReplaceWith;
 
   // find children by ex-page _id
   const children = await this.find({ parent: exPage._id });
@@ -454,21 +455,12 @@ schema.statics.getAggrConditionForPageWithProvidedPathAndDescendants = function(
  * add/subtract descendantCount of pages with provided paths by increment.
  * increment can be negative number
  */
-schema.statics.incrementDescendantCountOfPaths = async function(paths:string[], increment: number):Promise<void> {
-  const pages = await this.aggregate([{ $match: { path: { $in: paths } } }]);
-  const operations = pages.map((page) => {
-    return {
-      updateOne: {
-        filter: { path: page.path },
-        update: { descendantCount: page.descendantCount + increment },
-      },
-    };
-  });
-  await this.bulkWrite(operations);
+schema.statics.incrementDescendantCountOfPageIds = async function(pageIds: ObjectIdLike[], increment: number): Promise<void> {
+  await this.updateMany({ _id: { $in: pageIds } }, { $inc: { descendantCount: increment } });
 };
 
 // update descendantCount of a page with provided id
-schema.statics.recountDescendantCountOfSelfAndDescendants = async function(id:mongoose.Types.ObjectId):Promise<void> {
+schema.statics.recountDescendantCountOfSelfAndDescendants = async function(id: ObjectIdLike):Promise<void> {
   const res = await this.aggregate(
     [
       {
@@ -478,8 +470,8 @@ schema.statics.recountDescendantCountOfSelfAndDescendants = async function(id:mo
       },
       {
         $project: {
-          path: 1,
           parent: 1,
+          isEmpty: 1,
           descendantCount: 1,
         },
       },
@@ -490,7 +482,9 @@ schema.statics.recountDescendantCountOfSelfAndDescendants = async function(id:mo
             $sum: '$descendantCount',
           },
           sumOfDocsCount: {
-            $sum: 1,
+            $sum: {
+              $cond: { if: { $eq: ['$isEmpty', true] }, then: 0, else: 1 }, // exclude isEmpty true page from sumOfDocsCount
+            },
           },
         },
       },
@@ -508,6 +502,22 @@ schema.statics.recountDescendantCountOfSelfAndDescendants = async function(id:mo
   await this.findByIdAndUpdate(id, query);
 };
 
+schema.statics.findAncestorsUsingParentRecursively = async function(pageId: ObjectIdLike, shouldIncludeTarget: boolean) {
+  const self = this;
+  const target = await this.findById(pageId);
+
+  async function findAncestorsRecursively(target, ancestors = shouldIncludeTarget ? [target] : []) {
+    const parent = await self.findOne({ _id: target.parent });
+    if (parent == null) {
+      return ancestors;
+    }
+
+    return findAncestorsRecursively(parent, [...ancestors, parent]);
+  }
+
+  return findAncestorsRecursively(target);
+};
+
 export type PageCreateOptions = {
   format?: string
   grantUserGroupId?: ObjectIdLike
@@ -612,6 +622,8 @@ export default (crowi: Crowi): any => {
 
     let savedPage = await page.save();
 
+    await crowi.pageService?.updateDescendantCountOfAncestors(page._id, 1, false);
+
     /*
      * After save
      */

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

@@ -1209,7 +1209,12 @@ module.exports = function(crowi, app) {
         await crowi.pageService.deleteCompletely(page, req.user, options, isRecursively);
       }
       else {
-        if (!page.isEmpty && !page.isUpdatable(previousRevision)) {
+        const notRecursivelyAndEmpty = page.isEmpty && !isRecursively;
+        if (notRecursivelyAndEmpty) {
+          return res.json(ApiResponse.error(`Page '${pageId}' is not found.`, 'notfound'));
+        }
+
+        if (!page.isUpdatable(previousRevision)) {
           return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
         }
 

+ 122 - 45
packages/app/src/server/service/page.ts

@@ -343,9 +343,6 @@ class PageService {
       }
     }
 
-    // update descendants first
-    await this.renameDescendantsWithStream(page, newPagePath, user, options, shouldUseV4Process);
-
     /*
      * update target
      */
@@ -363,6 +360,10 @@ class PageService {
 
     this.pageEvent.emit('rename', page, user);
 
+    // TODO: resume
+    // update descendants first
+    this.renameDescendantsWithStream(page, newPagePath, user, options, shouldUseV4Process);
+
     return renamedPage;
   }
 
@@ -570,7 +571,7 @@ class PageService {
       .pipe(createBatchStream(BULK_REINDEX_SIZE))
       .pipe(writeStream);
 
-    await streamToPromise(readStream);
+    await streamToPromise(writeStream);
   }
 
   private async renameDescendantsWithStreamV4(targetPage, newPagePath, user, options = {}) {
@@ -679,10 +680,6 @@ class PageService {
       newPagePath, page.revision.body, user, options,
     );
 
-    if (isRecursively) {
-      this.duplicateDescendantsWithStream(page, newPagePath, user, shouldUseV4Process);
-    }
-
     // take over tags
     const originTags = await page.findRelatedTagsById();
     let savedTags = [];
@@ -695,6 +692,11 @@ class PageService {
     const result = serializePageSecurely(createdPage);
     result.tags = savedTags;
 
+    // TODO: resume
+    if (isRecursively) {
+      this.duplicateDescendantsWithStream(page, newPagePath, user, shouldUseV4Process);
+    }
+
     return result;
   }
 
@@ -893,7 +895,7 @@ class PageService {
 
     const duplicateDescendants = this.duplicateDescendants.bind(this);
     const shouldNormalizeParent = this.shouldNormalizeParent.bind(this);
-    const normalizeParentRecursively = this.normalizeParentRecursively.bind(this);
+    const normalizeParentAndDescendantCountOfDescendants = this.normalizeParentAndDescendantCountOfDescendants.bind(this);
     const pageEvent = this.pageEvent;
     let count = 0;
     const writeStream = new Writable({
@@ -915,9 +917,7 @@ class PageService {
         const shouldNormalize = shouldNormalizeParent(page);
         if (shouldNormalize) {
           try {
-            const escapedPath = escapeStringRegexp(newPagePath);
-            const regexps = [new RegExp(`^${escapedPath}`, 'i')];
-            await normalizeParentRecursively(null, regexps);
+            await normalizeParentAndDescendantCountOfDescendants(newPagePath);
             logger.info(`Successfully normalized duplicated descendant pages under "${newPagePath}"`);
           }
           catch (err) {
@@ -1004,20 +1004,19 @@ class PageService {
       throw new Error('Page is not deletable.');
     }
 
-    // replace with an empty page
-    const shouldReplace = !isRecursively && await Page.exists({ parent: page._id });
-    if (shouldReplace) {
-      await Page.replaceTargetWithPage(page);
-    }
-
-    if (isRecursively) {
-      this.deleteDescendantsWithStream(page, user, shouldUseV4Process); // use the same process in both version v4 and v5
-    }
-    else {
+    if (!isRecursively) {
       // replace with an empty page
       const shouldReplace = await Page.exists({ parent: page._id });
       if (shouldReplace) {
-        await Page.replaceTargetWithEmptyPage(page);
+        await Page.replaceTargetWithPage(page);
+      }
+
+      // update descendantCount of ancestors'
+      await this.updateDescendantCountOfAncestors(page.parent, -1, true);
+
+      const shouldDeleteLeafEmptyPages = !shouldReplace;
+      if (shouldDeleteLeafEmptyPages) {
+        // TODO https://redmine.weseek.co.jp/issues/87667 : delete leaf empty pages here
       }
     }
 
@@ -1030,7 +1029,7 @@ class PageService {
       await Revision.updateRevisionListByPageId(page._id, { pageId: page._id });
       deletedPage = await Page.findByIdAndUpdate(page._id, {
         $set: {
-          path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(), parent: null, // set parent as null
+          path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(), parent: null, descendantCount: 0, // set parent as null
         },
       }, { new: true });
       await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: true } });
@@ -1041,6 +1040,21 @@ class PageService {
       this.pageEvent.emit('create', deletedPage, user);
     }
 
+    // TODO: resume
+    // no await for deleteDescendantsWithStream and updateDescendantCountOfAncestors
+    if (isRecursively) {
+      (async() => {
+        const deletedDescendantCount = await this.deleteDescendantsWithStream(page, user, shouldUseV4Process); // use the same process in both version v4 and v5
+
+        // update descendantCount of ancestors'
+        if (page.parent != null) {
+          await this.updateDescendantCountOfAncestors(page.parent, (deletedDescendantCount + 1) * -1, true);
+
+          // TODO https://redmine.weseek.co.jp/issues/87667 : delete leaf empty pages here
+        }
+      })();
+    }
+
     return deletedPage;
   }
 
@@ -1108,7 +1122,7 @@ class PageService {
             filter: { _id: page._id },
             update: {
               $set: {
-                path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(), parent: null, // set parent as null
+                path: newPath, status: Page.STATUS_DELETED, deleteUser: user._id, deletedAt: Date.now(), parent: null, descendantCount: 0, // set parent as null
               },
             },
           },
@@ -1150,9 +1164,9 @@ class PageService {
   }
 
   /**
-   * Create delete stream
+   * Create delete stream and return deleted document count
    */
-  private async deleteDescendantsWithStream(targetPage, user, shouldUseV4Process = true) {
+  private async deleteDescendantsWithStream(targetPage, user, shouldUseV4Process = true): Promise<number> {
     let readStream;
     if (shouldUseV4Process) {
       readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
@@ -1165,9 +1179,13 @@ class PageService {
 
     const deleteDescendants = this.deleteDescendants.bind(this);
     let count = 0;
+    let nDeletedNonEmptyPages = 0; // used for updating descendantCount
+
     const writeStream = new Writable({
       objectMode: true,
       async write(batch, encoding, callback) {
+        nDeletedNonEmptyPages += batch.filter(d => !d.isEmpty).length;
+
         try {
           count += batch.length;
           await deleteDescendants(batch, user);
@@ -1189,6 +1207,10 @@ class PageService {
     readStream
       .pipe(createBatchStream(BULK_REINDEX_SIZE))
       .pipe(writeStream);
+
+    await streamToPromise(readStream);
+
+    return nDeletedNonEmptyPages;
   }
 
   private async deleteCompletelyOperation(pageIds, pagePaths) {
@@ -1257,14 +1279,31 @@ class PageService {
 
     await this.deleteCompletelyOperation(ids, paths);
 
-    if (isRecursively) {
-      this.deleteCompletelyDescendantsWithStream(page, user, options, shouldUseV4Process);
+    if (!isRecursively) {
+      await this.updateDescendantCountOfAncestors(page.parent, -1, true);
+
+      // TODO https://redmine.weseek.co.jp/issues/87667 : delete leaf empty pages here
     }
 
     if (!page.isEmpty && !preventEmitting) {
       this.pageEvent.emit('deleteCompletely', page, user);
     }
 
+    // TODO: resume
+    if (isRecursively) {
+      // no await for deleteCompletelyDescendantsWithStream
+      (async() => {
+        const deletedDescendantCount = await this.deleteCompletelyDescendantsWithStream(page, user, options, shouldUseV4Process);
+
+        // update descendantCount of ancestors'
+        if (page.parent != null) {
+          await this.updateDescendantCountOfAncestors(page.parent, (deletedDescendantCount + 1) * -1, true);
+        }
+
+        // TODO https://redmine.weseek.co.jp/issues/87667 : delete leaf empty pages here
+      })();
+    }
+
     return;
   }
 
@@ -1294,7 +1333,7 @@ class PageService {
   /**
    * Create delete completely stream
    */
-  private async deleteCompletelyDescendantsWithStream(targetPage, user, options = {}, shouldUseV4Process = true) {
+  private async deleteCompletelyDescendantsWithStream(targetPage, user, options = {}, shouldUseV4Process = true): Promise<number> {
     let readStream;
 
     if (shouldUseV4Process) { // pages don't have parents
@@ -1305,11 +1344,15 @@ class PageService {
       readStream = await factory.generateReadable();
     }
 
-    const deleteMultipleCompletely = this.deleteMultipleCompletely.bind(this);
     let count = 0;
+    let nDeletedNonEmptyPages = 0; // used for updating descendantCount
+
+    const deleteMultipleCompletely = this.deleteMultipleCompletely.bind(this);
     const writeStream = new Writable({
       objectMode: true,
       async write(batch, encoding, callback) {
+        nDeletedNonEmptyPages += batch.filter(d => !d.isEmpty).length;
+
         try {
           count += batch.length;
           await deleteMultipleCompletely(batch, user, options);
@@ -1331,6 +1374,10 @@ class PageService {
     readStream
       .pipe(createBatchStream(BULK_REINDEX_SIZE))
       .pipe(writeStream);
+
+    await streamToPromise(readStream);
+
+    return nDeletedNonEmptyPages;
   }
 
   // use the same process in both v4 and v5
@@ -1394,13 +1441,28 @@ class PageService {
     page.lastUpdateUser = user;
     const updatedPage = await Page.findByIdAndUpdate(page._id, {
       $set: {
-        path: newPath, status: Page.STATUS_PUBLISHED, lastUpdateUser: user._id, deleteUser: null, deletedAt: null, parent: parent._id,
+        path: newPath, status: Page.STATUS_PUBLISHED, lastUpdateUser: user._id, deleteUser: null, deletedAt: null, parent: parent._id, descendantCount: 0,
       },
     }, { new: true });
     await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
 
     if (isRecursively) {
-      this.revertDeletedDescendantsWithStream(page, user, options, shouldUseV4Process);
+      await this.updateDescendantCountOfAncestors(parent._id, 1, true);
+    }
+
+    // TODO: resume
+    if (!isRecursively) {
+      // no await for revertDeletedDescendantsWithStream
+      (async() => {
+        const revertedDescendantCount = await this.revertDeletedDescendantsWithStream(page, user, options, shouldUseV4Process);
+
+        // update descendantCount of ancestors'
+        if (page.parent != null) {
+          await this.updateDescendantCountOfAncestors(page.parent, revertedDescendantCount + 1, true);
+
+          // TODO https://redmine.weseek.co.jp/issues/87667 : delete leaf empty pages here
+        }
+      })();
     }
 
     return updatedPage;
@@ -1436,7 +1498,7 @@ class PageService {
   /**
    * Create revert stream
    */
-  private async revertDeletedDescendantsWithStream(targetPage, user, options = {}, shouldUseV4Process = true) {
+  private async revertDeletedDescendantsWithStream(targetPage, user, options = {}, shouldUseV4Process = true): Promise<number> {
     if (shouldUseV4Process) {
       return this.revertDeletedDescendantsWithStreamV4(targetPage, user, options);
     }
@@ -1444,7 +1506,7 @@ class PageService {
     const readStream = await this.generateReadStreamToOperateOnlyDescendants(targetPage.path, user);
 
     const revertDeletedDescendants = this.revertDeletedDescendants.bind(this);
-    const normalizeParentRecursively = this.normalizeParentRecursively.bind(this);
+    const normalizeParentAndDescendantCountOfDescendants = this.normalizeParentAndDescendantCountOfDescendants.bind(this);
     const shouldNormalizeParent = this.shouldNormalizeParent.bind(this);
     let count = 0;
     const writeStream = new Writable({
@@ -1468,9 +1530,7 @@ class PageService {
         if (shouldNormalize) {
           try {
             const newPath = Page.getRevertDeletedPageName(targetPage.path);
-            const escapedPath = escapeStringRegexp(newPath);
-            const regexps = [new RegExp(`^${escapedPath}`, 'i')];
-            await normalizeParentRecursively(null, regexps);
+            await normalizeParentAndDescendantCountOfDescendants(newPath);
             logger.info(`Successfully normalized reverted descendant pages under "${newPath}"`);
           }
           catch (err) {
@@ -1487,6 +1547,10 @@ class PageService {
     readStream
       .pipe(createBatchStream(BULK_REINDEX_SIZE))
       .pipe(writeStream);
+
+    await streamToPromise(readStream);
+
+    return count;
   }
 
   private async revertDeletedDescendantsWithStreamV4(targetPage, user, options = {}) {
@@ -1518,6 +1582,10 @@ class PageService {
     readStream
       .pipe(createBatchStream(BULK_REINDEX_SIZE))
       .pipe(writeStream);
+
+    await streamToPromise(readStream);
+
+    return count;
   }
 
 
@@ -1858,6 +1926,15 @@ class PageService {
     }
   }
 
+  private async normalizeParentAndDescendantCountOfDescendants(path: string): Promise<void> {
+    const escapedPath = escapeStringRegexp(path);
+    const regexps = [new RegExp(`^${escapedPath}`, 'i')];
+    await this.normalizeParentRecursively(null, regexps);
+
+    // update descendantCount of descendant pages
+    await this.updateDescendantCountOfSelfAndDescendants(path);
+  }
+
   // TODO: use websocket to show progress
   private async normalizeParentRecursively(grant, regexps, publicOnly = false): Promise<void> {
     const BATCH_SIZE = 100;
@@ -2046,7 +2123,7 @@ class PageService {
    * - page that has the same path as the provided path
    * - pages that are descendants of the above page
    */
-  async updateDescendantCountOfSelfAndDescendants(path = '/') {
+  async updateDescendantCountOfSelfAndDescendants(path) {
     const BATCH_SIZE = 200;
     const Page = this.crowi.model('Page');
 
@@ -2056,8 +2133,7 @@ class PageService {
     const recountWriteStream = new Writable({
       objectMode: true,
       async write(pageDocuments, encoding, callback) {
-        for (const document of pageDocuments) {
-          // eslint-disable-next-line no-await-in-loop
+        for await (const document of pageDocuments) {
           await Page.recountDescendantCountOfSelfAndDescendants(document._id);
         }
         callback();
@@ -2073,11 +2149,12 @@ class PageService {
     await streamToPromise(recountWriteStream);
   }
 
-  // update descendantCount of all pages that are ancestors of a provided path by count
-  async updateDescendantCountOfAncestors(path = '/', count = 0) {
+  // update descendantCount of all pages that are ancestors of a provided pageId by count
+  async updateDescendantCountOfAncestors(pageId: ObjectIdLike, inc: number, shouldIncludeTarget: boolean): Promise<void> {
     const Page = this.crowi.model('Page');
-    const ancestors = collectAncestorPaths(path);
-    await Page.incrementDescendantCountOfPaths(ancestors, count);
+    const ancestors = await Page.findAncestorsUsingParentRecursively(pageId, shouldIncludeTarget);
+    const ancestorPageIds = ancestors.map(p => p._id);
+    await Page.incrementDescendantCountOfPageIds(ancestorPageIds, inc);
   }
 
 }

+ 1 - 2
packages/app/src/server/util/swigFunctions.js

@@ -146,8 +146,7 @@ module.exports = function(crowi, req, locals) {
     return false;
   };
 
-  locals.isTrashPage = function() {
-    const path = req.path || '';
+  locals.isTrashPage = function(path = '') {
     if (path.match(/^\/trash(\/.*)?$/)) {
       return true;
     }

+ 1 - 1
packages/app/src/server/views/layout-growi/page_list.html

@@ -13,7 +13,7 @@
 
 
 {% block content_main_after %}
-  {% if isTrashPage() %}
+  {% if isTrashPage(page.path) %}
     <div class="grw-container-convertible">
       <div id="trash-page-list"></div>
     </div>

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

@@ -73,7 +73,7 @@
     </div>
     {% endif %}
 
-    {% if isTrashPage() %}
+    {% if isTrashPage(page.path) %}
       <div id="trash-page-alert"></div>
     {% endif %}
   </div>