Преглед изворни кода

Merge pull request #5888 from weseek/feat/inject-shouldFix-info-to-page-items-for-page-tree

feat: Inject PageOperation info to page items for page tree
Yohei Shiina пре 3 година
родитељ
комит
aa1214335a

+ 6 - 2
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -8,6 +8,7 @@ import {
 import {
   IPageInfoAll, isIPageInfoForOperation,
 } from '~/interfaces/page';
+import { IPageOperationProcessData } from '~/interfaces/page-operation';
 import { useSWRxPageInfo } from '~/stores/page';
 import loggerFactory from '~/utils/logger';
 
@@ -48,6 +49,7 @@ type CommonProps = {
 type DropdownMenuProps = CommonProps & {
   pageId: string,
   isLoading?: boolean,
+  operationProcessData?: IPageOperationProcessData,
 }
 
 const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.Element => {
@@ -55,7 +57,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
 
   const {
     pageId, isLoading,
-    pageInfo, isEnableActions, forceHideMenuItems,
+    pageInfo, isEnableActions, forceHideMenuItems, operationProcessData,
     onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDuplicateMenuItem, onClickDeleteMenuItem, onClickRevertMenuItem, onClickPathRecoveryMenuItem,
     additionalMenuItemRenderer: AdditionalMenuItems, isInstantRename,
   } = props;
@@ -195,7 +197,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
         ) }
 
         {/* PathRecovery */}
-        { !forceHideMenuItems?.includes(MenuItemType.PATH_RECOVERY) && isEnableActions && (
+        { !forceHideMenuItems?.includes(MenuItemType.PATH_RECOVERY) && isEnableActions && operationProcessData?.Rename != null && (
           <DropdownItem
             onClick={pathRecoveryItemClickedHandler}
             className="grw-page-control-dropdown-item"
@@ -237,6 +239,7 @@ type PageItemControlSubstanceProps = CommonProps & {
   pageId: string,
   fetchOnInit?: boolean,
   children?: React.ReactNode,
+  operationProcessData?: IPageOperationProcessData,
 }
 
 export const PageItemControlSubstance = (props: PageItemControlSubstanceProps): JSX.Element => {
@@ -322,6 +325,7 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
 type PageItemControlProps = CommonProps & {
   pageId?: string,
   children?: React.ReactNode,
+  operationProcessData?: IPageOperationProcessData,
 }
 
 export const PageItemControl = (props: PageItemControlProps): JSX.Element => {

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

@@ -457,6 +457,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
             onClickRenameMenuItem={renameMenuItemClickHandler}
             onClickDeleteMenuItem={deleteMenuItemClickHandler}
             isInstantRename
+            operationProcessData={page.processData}
           >
             {/* pass the color property to reactstrap dropdownToggle props. https://6-4-0--reactstrap.netlify.app/components/dropdowns/  */}
             <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">

+ 15 - 0
packages/app/src/interfaces/page-operation.ts

@@ -0,0 +1,15 @@
+export const PageActionType = {
+  Rename: 'Rename',
+  Duplicate: 'Duplicate',
+  Delete: 'Delete',
+  DeleteCompletely: 'DeleteCompletely',
+  Revert: 'Revert',
+  NormalizeParent: 'NormalizeParent',
+} as const;
+export type PageActionType = typeof PageActionType[keyof typeof PageActionType]
+export type IPageOperationProcessData = Partial<{
+  [key in PageActionType]: {isProcessable: boolean}
+}>
+export type IPageOperationProcessInfo = {
+  [pageId: string]: IPageOperationProcessData,
+}

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

@@ -1,5 +1,6 @@
 import { Ref, Nullable } from './common';
 import { HasObjectId } from './has-object-id';
+import { IPageOperationProcessData } from './page-operation';
 import { IRevision, HasRevisionShortbody } from './revision';
 import { SubscriptionStatusType } from './subscription';
 import { ITag } from './tag';
@@ -43,7 +44,7 @@ export type PageGrant = typeof PageGrant[keyof typeof PageGrant];
 
 export type IPageHasId = IPage & HasObjectId;
 
-export type IPageForItem = Partial<IPageHasId & {isTarget?: boolean}>;
+export type IPageForItem = Partial<IPageHasId & {isTarget?: boolean, processData?: IPageOperationProcessData}>;
 
 export type IPageInfo = {
   isV5Compatible: boolean,

+ 7 - 6
packages/app/src/server/models/page-operation.ts

@@ -47,6 +47,8 @@ export interface IPageOperation {
   options?: IOptionsForResuming,
   incForUpdatingDescendantCount?: number,
   unprocessableExpiryDate: Date,
+
+  isProcessable(): boolean
 }
 
 export interface PageOperationDocument extends IPageOperation, Document {}
@@ -57,7 +59,6 @@ export interface PageOperationModel extends Model<PageOperationDocument> {
   findByIdAndUpdatePageActionStage(pageOpId: ObjectIdLike, stage: PageActionStage): Promise<PageOperationDocumentHasId | null>
   findMainOps(filter?: FilterQuery<PageOperationDocument>, projection?: any, options?: QueryOptions): Promise<PageOperationDocumentHasId[]>
   deleteByActionTypes(deleteTypeList: PageActionType[]): Promise<void>
-  isProcessable(pageOp: PageOperationDocument): boolean
   extendExpiryDate(operationId: ObjectIdLike): Promise<void>
 }
 
@@ -136,11 +137,6 @@ schema.statics.deleteByActionTypes = async function(
   logger.info(`Deleted all PageOperation documents with actionType: [${actionTypes}]`);
 };
 
-schema.statics.isProcessable = function(pageOp: PageOperationDocument): boolean {
-  const { unprocessableExpiryDate } = pageOp;
-  return unprocessableExpiryDate == null || (unprocessableExpiryDate != null && new Date() > unprocessableExpiryDate);
-};
-
 /**
  * add TIME_TO_ADD_SEC to current time and update unprocessableExpiryDate with it
  */
@@ -149,4 +145,9 @@ schema.statics.extendExpiryDate = async function(operationId: ObjectIdLike): Pro
   await this.findByIdAndUpdate(operationId, { unprocessableExpiryDate: date });
 };
 
+schema.methods.isProcessable = function(): boolean {
+  const { unprocessableExpiryDate } = this;
+  return unprocessableExpiryDate == null || (unprocessableExpiryDate != null && new Date() > unprocessableExpiryDate);
+};
+
 export default getOrCreateModel<PageOperationDocument, PageOperationModel>('PageOperation', schema);

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

@@ -1,6 +1,7 @@
 import { pagePathUtils } from '@growi/core';
 
-import PageOperation, { PageActionType } from '~/server/models/page-operation';
+import { IPageOperationProcessInfo, IPageOperationProcessData } from '~/interfaces/page-operation';
+import PageOperation, { PageActionType, PageOperationDocument } from '~/server/models/page-operation';
 
 import { ObjectIdLike } from '../interfaces/mongoose-utils';
 
@@ -80,6 +81,35 @@ class PageOperationService {
     return true;
   }
 
+  /**
+   * Generate object that connects page id with processData of PageOperation.
+   * The processData is a combination of actionType as a key and information on whether the action is processable as a value.
+   */
+  generateProcessInfo(pageOps: PageOperationDocument[]): IPageOperationProcessInfo {
+    const processInfo: IPageOperationProcessInfo = {};
+
+    pageOps.forEach((pageOp) => {
+      const pageId = pageOp.page._id.toString();
+
+      const actionType = pageOp.actionType;
+      const isProcessable = pageOp.isProcessable();
+
+      // processData for processInfo
+      const processData: IPageOperationProcessData = { [actionType]: { isProcessable } };
+
+      // Merge processData if other processData exist
+      if (processInfo[pageId] != null) {
+        const otherProcessData = processInfo[pageId];
+        processInfo[pageId] = { ...otherProcessData, ...processData };
+        return;
+      }
+      // add new process data to processInfo
+      processInfo[pageId] = processData;
+    });
+
+    return processInfo;
+  }
+
   /**
    * Set interval to update unprocessableExpiryDate every AUTO_UPDATE_INTERVAL_SEC seconds.
    * This is used to prevent the same page operation from being processed multiple times at once

+ 46 - 10
packages/app/src/server/service/page.ts

@@ -16,6 +16,7 @@ import {
 import {
   PageDeleteConfigValue, IPageDeleteConfigValueToProcessValidation,
 } from '~/interfaces/page-delete-config';
+import { IPageOperationProcessInfo, IPageOperationProcessData } from '~/interfaces/page-operation';
 import { IUserHasId } from '~/interfaces/user';
 import { PageMigrationErrorData, SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
 import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
@@ -602,7 +603,7 @@ class PageService {
     if (pageOp == null) {
       throw Error('There is nothing to be processed right now');
     }
-    const isProcessable = await PageOperation.isProcessable(pageOp);
+    const isProcessable = pageOp.isProcessable();
     if (!isProcessable) {
       throw Error('This page operation is currently being processed');
     }
@@ -3507,11 +3508,15 @@ class PageService {
     }
     await queryBuilder.addViewerCondition(user, userGroups);
 
-    return queryBuilder
+    const pages = await queryBuilder
       .addConditionToSortPagesByAscPath()
       .query
       .lean()
       .exec();
+
+    await this.injectProcessDataIntoPagesByActionTypes(pages, [PageActionType.Rename]);
+
+    return pages;
   }
 
   async findAncestorsChildrenByPathAndViewer(path: string, user, userGroups = null): Promise<Record<string, PageDocument[]>> {
@@ -3523,20 +3528,16 @@ class PageService {
     // get pages at once
     const queryBuilder = new PageQueryBuilder(Page.find({ path: { $in: regexps } }), true);
     await queryBuilder.addViewerCondition(user, userGroups);
-    const _pages = await queryBuilder
+    const pages = await queryBuilder
       .addConditionAsOnTree()
       .addConditionToMinimizeDataForRendering()
       .addConditionToSortPagesByAscPath()
       .query
       .lean()
       .exec();
-    // mark target
-    const pages = _pages.map((page: PageDocument & { isTarget?: boolean }) => {
-      if (page.path === path) {
-        page.isTarget = true;
-      }
-      return page;
-    });
+
+    this.injectIsTargetIntoPages(pages, path);
+    await this.injectProcessDataIntoPagesByActionTypes(pages, [PageActionType.Rename]);
 
     /*
      * If any non-migrated page is found during creating the pathToChildren map, it will stop incrementing at that moment
@@ -3555,6 +3556,41 @@ class PageService {
     return pathToChildren;
   }
 
+  private injectIsTargetIntoPages(pages: (PageDocument & {isTarget?: boolean})[], path): void {
+    pages.forEach((page) => {
+      if (page.path === path) {
+        page.isTarget = true;
+      }
+    });
+  }
+
+  /**
+   * Inject processData into page docuements
+   * The processData is a combination of actionType as a key and information on whether the action is processable as a value.
+   */
+  private async injectProcessDataIntoPagesByActionTypes(
+      pages: (PageDocument & { processData?: IPageOperationProcessData })[],
+      actionTypes: PageActionType[],
+  ): Promise<void> {
+
+    const pageOperations = await PageOperation.find({ actionType: { $in: actionTypes } });
+    if (pageOperations == null || pageOperations.length === 0) {
+      return;
+    }
+
+    const processInfo: IPageOperationProcessInfo = this.crowi.pageOperationService.generateProcessInfo(pageOperations);
+    const operatingPageIds: string[] = Object.keys(processInfo);
+
+    // inject processData into pages
+    pages.forEach((page) => {
+      const pageId = page._id.toString();
+      if (operatingPageIds.includes(pageId)) {
+        const processData: IPageOperationProcessData = processInfo[pageId];
+        page.processData = processData;
+      }
+    });
+  }
+
 }
 
 export default PageService;