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

Merge pull request #5444 from weseek/fix/move-to-deep-descendant

fix: Rename to under descendant
Yuki Takei 4 лет назад
Родитель
Сommit
44a15dbce6

+ 3 - 2
packages/app/src/server/routes/apiv3/pages.js

@@ -492,6 +492,7 @@ module.exports = (crowi) => {
     }
     }
 
 
     let page;
     let page;
+    let renamedPage;
 
 
     try {
     try {
       page = await Page.findByIdAndViewerToEdit(pageId, req.user, true);
       page = await Page.findByIdAndViewerToEdit(pageId, req.user, true);
@@ -508,14 +509,14 @@ module.exports = (crowi) => {
       if (!page.isEmpty && !page.isUpdatable(revisionId)) {
       if (!page.isEmpty && !page.isUpdatable(revisionId)) {
         return res.apiv3Err(new ErrorV3('Someone could update this page, so couldn\'t delete.', 'notfound_or_forbidden'), 409);
         return res.apiv3Err(new ErrorV3('Someone could update this page, so couldn\'t delete.', 'notfound_or_forbidden'), 409);
       }
       }
-      page = await crowi.pageService.renamePage(page, newPagePath, req.user, options);
+      renamedPage = await crowi.pageService.renamePage(page, newPagePath, req.user, options);
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);
       return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
       return res.apiv3Err(new ErrorV3('Failed to update page.', 'unknown'), 500);
     }
     }
 
 
-    const result = { page: serializePageSecurely(page) };
+    const result = { page: serializePageSecurely(renamedPage ?? page) };
 
 
     try {
     try {
       // global notification
       // global notification

+ 1 - 1
packages/app/src/server/service/page-operation.ts

@@ -12,7 +12,7 @@ class PageOperationService {
     this.crowi = crowi;
     this.crowi = crowi;
 
 
     // TODO: Remove this code when resuming feature is implemented
     // TODO: Remove this code when resuming feature is implemented
-    PageOperation.deleteMany();
+    PageOperation.deleteMany({});
   }
   }
 
 
   /**
   /**

+ 91 - 12
packages/app/src/server/service/page.ts

@@ -47,7 +47,7 @@ class PageCursorsForDescendantsFactory {
 
 
   private shouldIncludeEmpty: boolean;
   private shouldIncludeEmpty: boolean;
 
 
-  private initialCursor: QueryCursor<any>; // TODO: wait for mongoose update
+  private initialCursor: QueryCursor<any> | never[]; // TODO: wait for mongoose update
 
 
   private Page: PageModel;
   private Page: PageModel;
 
 
@@ -69,11 +69,11 @@ class PageCursorsForDescendantsFactory {
    * Returns Iterable that yields only descendant pages unorderedly
    * Returns Iterable that yields only descendant pages unorderedly
    * @returns Promise<AsyncGenerator>
    * @returns Promise<AsyncGenerator>
    */
    */
-  async generateIterable(): Promise<AsyncGenerator> {
+  async generateIterable(): Promise<AsyncGenerator | never[]> {
     // initialize cursor
     // initialize cursor
     await this.init();
     await this.init();
 
 
-    return this.generateOnlyDescendants(this.initialCursor);
+    return this.isNeverArray(this.initialCursor) ? [] : this.generateOnlyDescendants(this.initialCursor);
   }
   }
 
 
   /**
   /**
@@ -90,13 +90,19 @@ class PageCursorsForDescendantsFactory {
   private async* generateOnlyDescendants(cursor: QueryCursor<any>) {
   private async* generateOnlyDescendants(cursor: QueryCursor<any>) {
     for await (const page of cursor) {
     for await (const page of cursor) {
       const nextCursor = await this.generateCursorToFindChildren(page);
       const nextCursor = await this.generateCursorToFindChildren(page);
-      yield* this.generateOnlyDescendants(nextCursor); // recursively yield
+      if (!this.isNeverArray(nextCursor)) {
+        yield* this.generateOnlyDescendants(nextCursor); // recursively yield
+      }
 
 
       yield page;
       yield page;
     }
     }
   }
   }
 
 
-  private async generateCursorToFindChildren(page: any): Promise<QueryCursor<any>> {
+  private async generateCursorToFindChildren(page: any): Promise<QueryCursor<any> | never[]> {
+    if (page == null) {
+      return [];
+    }
+
     const { PageQueryBuilder } = this.Page;
     const { PageQueryBuilder } = this.Page;
 
 
     const builder = new PageQueryBuilder(this.Page.find(), this.shouldIncludeEmpty);
     const builder = new PageQueryBuilder(this.Page.find(), this.shouldIncludeEmpty);
@@ -108,6 +114,10 @@ class PageCursorsForDescendantsFactory {
     return cursor;
     return cursor;
   }
   }
 
 
+  private isNeverArray(val: QueryCursor<any> | never[]): val is never[] {
+    return 'length' in val && val.length === 0;
+  }
+
 }
 }
 
 
 class PageService {
 class PageService {
@@ -444,11 +454,17 @@ class PageService {
     await Page.takeOffFromTree(page._id);
     await Page.takeOffFromTree(page._id);
 
 
     // 2. Find new parent
     // 2. Find new parent
-    const update: Partial<IPage> = {};
-    // find or create parent
-    const newParent = await Page.getParentAndFillAncestors(newPagePath);
+    let newParent;
+    // If renaming to under target, run getParentAndforceCreateEmptyTree to fill new ancestors
+    if (this.isRenamingToUnderTarget(page.path, newPagePath)) {
+      newParent = await this.getParentAndforceCreateEmptyTree(page, newPagePath);
+    }
+    else {
+      newParent = await Page.getParentAndFillAncestors(newPagePath);
+    }
 
 
     // 3. Put back target page to tree (also update the other attrs)
     // 3. Put back target page to tree (also update the other attrs)
+    const update: Partial<IPage> = {};
     update.path = newPagePath;
     update.path = newPagePath;
     update.parent = newParent._id;
     update.parent = newParent._id;
     if (updateMetadata) {
     if (updateMetadata) {
@@ -495,10 +511,7 @@ class PageService {
     await this.updateDescendantCountOfAncestors(renamedPage._id, nToIncrease, false);
     await this.updateDescendantCountOfAncestors(renamedPage._id, nToIncrease, false);
 
 
     // Remove leaf empty pages if not moving to under the ex-target position
     // Remove leaf empty pages if not moving to under the ex-target position
-    const pathToTest = escapeStringRegexp(addTrailingSlash(page.path));
-    const pathToBeTested = newPagePath;
-    const isRenamingToUnderExTarget = (new RegExp(`^${pathToTest}`)).test(pathToBeTested);
-    if (!isRenamingToUnderExTarget) {
+    if (!this.isRenamingToUnderTarget(page.path, newPagePath)) {
       // remove empty pages at leaf position
       // remove empty pages at leaf position
       await Page.removeLeafEmptyPagesRecursively(page.parent);
       await Page.removeLeafEmptyPagesRecursively(page.parent);
     }
     }
@@ -506,6 +519,72 @@ class PageService {
     await PageOperation.findByIdAndDelete(pageOpId);
     await PageOperation.findByIdAndDelete(pageOpId);
   }
   }
 
 
+  private isRenamingToUnderTarget(fromPath: string, toPath: string): boolean {
+    const pathToTest = escapeStringRegexp(addTrailingSlash(fromPath));
+    const pathToBeTested = toPath;
+
+    return (new RegExp(`^${pathToTest}`, 'i')).test(pathToBeTested);
+  }
+
+  private async getParentAndforceCreateEmptyTree(originalPage, toPath: string) {
+    const Page = mongoose.model('Page') as unknown as PageModel;
+
+    const fromPath = originalPage.path;
+    const newParentPath = pathlib.dirname(toPath);
+
+    // local util
+    const collectAncestorPathsUntilFromPath = (path: string, paths: string[] = [path]): string[] => {
+      const nextPath = pathlib.dirname(path);
+      if (nextPath === fromPath) {
+        return [...paths, nextPath];
+      }
+
+      paths.push(nextPath);
+
+      return collectAncestorPathsUntilFromPath(nextPath, paths);
+    };
+
+    const pathsToInsert = collectAncestorPathsUntilFromPath(newParentPath);
+    const originalParent = await Page.findById(originalPage.parent);
+    if (originalParent == null) {
+      throw Error('Original parent not found');
+    }
+    const insertedPages = await Page.insertMany(pathsToInsert.map((path) => {
+      return {
+        path,
+        isEmpty: true,
+      };
+    }));
+
+    const pages = [...insertedPages, originalParent];
+
+    const ancestorsMap = new Map<string, PageDocument & {_id: any}>(pages.map(p => [p.path, p]));
+
+    // bulkWrite to update ancestors
+    const operations = insertedPages.map((page) => {
+      const parentPath = pathlib.dirname(page.path);
+      const op = {
+        updateOne: {
+          filter: {
+            _id: page._id,
+          },
+          update: {
+            $set: {
+              parent: ancestorsMap.get(parentPath)?._id,
+              descedantCount: originalParent.descendantCount,
+            },
+          },
+        },
+      };
+
+      return op;
+    });
+    await Page.bulkWrite(operations);
+
+    const newParent = ancestorsMap.get(newParentPath);
+    return newParent;
+  }
+
   // !!renaming always include descendant pages!!
   // !!renaming always include descendant pages!!
   private async renamePageV4(page, newPagePath, user, options) {
   private async renamePageV4(page, newPagePath, user, options) {
     const Page = this.crowi.model('Page');
     const Page = this.crowi.model('Page');