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

Merge pull request #6044 from weseek/create-notification-when-parentPages-are-renamed

feat: The users get notified when the descendant pages are renamed
Shun Murai 3 лет назад
Родитель
Сommit
62183e58d7

+ 4 - 0
packages/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -119,6 +119,10 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
       actionMsg = 'reverted';
       actionMsg = 'reverted';
       actionIcon = 'icon-action-undo';
       actionIcon = 'icon-action-undo';
       break;
       break;
+    case 'PAGE_RECURSIVELY_RENAME':
+      actionMsg = 'renamed under';
+      actionIcon = 'icon-action-redo';
+      break;
     case 'COMMENT_CREATE':
     case 'COMMENT_CREATE':
       actionMsg = 'commented on';
       actionMsg = 'commented on';
       actionIcon = 'icon-bubble';
       actionIcon = 'icon-bubble';

+ 6 - 0
packages/app/src/interfaces/activity.ts

@@ -14,6 +14,9 @@ const ACTION_PAGE_DELETE_COMPLETELY = 'PAGE_DELETE_COMPLETELY';
 const ACTION_PAGE_REVERT = 'PAGE_REVERT';
 const ACTION_PAGE_REVERT = 'PAGE_REVERT';
 const ACTION_COMMENT_CREATE = 'COMMENT_CREATE';
 const ACTION_COMMENT_CREATE = 'COMMENT_CREATE';
 const ACTION_COMMENT_UPDATE = 'COMMENT_UPDATE';
 const ACTION_COMMENT_UPDATE = 'COMMENT_UPDATE';
+const ACTION_PAGE_RECURSIVELY_RENAME = 'PAGE_RECURSIVELY_RENAME';
+const ACTION_PAGE_RECURSIVELY_DELETE = 'PAGE_RECURSIVELY_DELETE';
+const ACTION_PAGE_RECURSIVELY_DELETE_COMPLETELY = 'PAGE_RECURSIVELY_DELETE_COMPLETELY';
 
 
 
 
 export const SUPPORTED_TARGET_MODEL_TYPE = {
 export const SUPPORTED_TARGET_MODEL_TYPE = {
@@ -36,6 +39,9 @@ export const SUPPORTED_ACTION_TYPE = {
   ACTION_PAGE_REVERT,
   ACTION_PAGE_REVERT,
   ACTION_COMMENT_CREATE,
   ACTION_COMMENT_CREATE,
   ACTION_COMMENT_UPDATE,
   ACTION_COMMENT_UPDATE,
+  ACTION_PAGE_RECURSIVELY_RENAME,
+  ACTION_PAGE_RECURSIVELY_DELETE,
+  ACTION_PAGE_RECURSIVELY_DELETE_COMPLETELY,
 } as const;
 } as const;
 
 
 
 

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

@@ -72,10 +72,9 @@ activitySchema.methods.getNotificationTargetUsers = async function() {
   const { user: actionUser, target } = this;
   const { user: actionUser, target } = this;
 
 
   const [subscribeUsers, unsubscribeUsers] = await Promise.all([
   const [subscribeUsers, unsubscribeUsers] = await Promise.all([
-    Subscription.getSubscription((target as any) as Types.ObjectId),
-    Subscription.getUnsubscription((target as any) as Types.ObjectId),
+    Subscription.getSubscription(target),
+    Subscription.getUnsubscription(target),
   ]);
   ]);
-
   const unique = array => Object.values(array.reduce((objects, object) => ({ ...objects, [object.toString()]: object }), {}));
   const unique = array => Object.values(array.reduce((objects, object) => ({ ...objects, [object.toString()]: object }), {}));
   const filter = (array, pull) => {
   const filter = (array, pull) => {
     const ids = pull.map(object => object.toString());
     const ids = pull.map(object => object.toString());

+ 10 - 5
packages/app/src/server/models/subscription.ts

@@ -10,7 +10,7 @@ import { SubscriptionStatusType, AllSubscriptionStatusType } from '~/interfaces/
 export interface ISubscription {
 export interface ISubscription {
   user: Types.ObjectId
   user: Types.ObjectId
   targetModel: string
   targetModel: string
-  target: Types.ObjectId
+  target: Types.ObjectId | Document
   status: string
   status: string
   createdAt: Date
   createdAt: Date
 
 
@@ -24,8 +24,9 @@ export interface SubscriptionModel extends Model<SubscriptionDocument> {
   findByUserIdAndTargetId(userId: Types.ObjectId | string, targetId: Types.ObjectId | string): any
   findByUserIdAndTargetId(userId: Types.ObjectId | string, targetId: Types.ObjectId | string): any
   upsertSubscription(user: Types.ObjectId, targetModel: string, target: Types.ObjectId, status: string): any
   upsertSubscription(user: Types.ObjectId, targetModel: string, target: Types.ObjectId, status: string): any
   subscribeByPageId(user: Types.ObjectId, pageId: Types.ObjectId, status: string): any
   subscribeByPageId(user: Types.ObjectId, pageId: Types.ObjectId, status: string): any
-  getSubscription(target: Types.ObjectId): Promise<Types.ObjectId[]>
-  getUnsubscription(target: Types.ObjectId): Promise<Types.ObjectId[]>
+  getSubscription(target: Types.ObjectId | Document): Promise<Types.ObjectId[]>
+  getUnsubscription(target: Types.ObjectId | Document): Promise<Types.ObjectId[]>
+  getSubscriptions(targets: Types.ObjectId[] | Document[]): Promise<Types.ObjectId[]>
 }
 }
 
 
 const subscriptionSchema = new Schema<SubscriptionDocument, SubscriptionModel>({
 const subscriptionSchema = new Schema<SubscriptionDocument, SubscriptionModel>({
@@ -79,12 +80,16 @@ subscriptionSchema.statics.subscribeByPageId = function(user, pageId, status) {
   return this.upsertSubscription(user, 'Page', pageId, status);
   return this.upsertSubscription(user, 'Page', pageId, status);
 };
 };
 
 
-subscriptionSchema.statics.getSubscription = async function(target) {
+subscriptionSchema.statics.getSubscription = async function(target: Types.ObjectId | Document) {
   return this.find({ target, status: SubscriptionStatusType.SUBSCRIBE }).distinct('user');
   return this.find({ target, status: SubscriptionStatusType.SUBSCRIBE }).distinct('user');
 };
 };
 
 
-subscriptionSchema.statics.getUnsubscription = async function(target) {
+subscriptionSchema.statics.getUnsubscription = async function(target: Types.ObjectId | Document) {
   return this.find({ target, status: SubscriptionStatusType.UNSUBSCRIBE }).distinct('user');
   return this.find({ target, status: SubscriptionStatusType.UNSUBSCRIBE }).distinct('user');
 };
 };
 
 
+subscriptionSchema.statics.getSubscriptions = async function(targets: Document[] | Types.ObjectId[]) {
+  return this.find({ $in: targets, status: SubscriptionStatusType.SUBSCRIBE }).distinct('user');
+};
+
 export default getOrCreateModel<SubscriptionDocument, SubscriptionModel>('Subscription', subscriptionSchema);
 export default getOrCreateModel<SubscriptionDocument, SubscriptionModel>('Subscription', subscriptionSchema);

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

@@ -538,9 +538,7 @@ module.exports = (crowi) => {
       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(renamedPage ?? page) };
     const result = { page: serializePageSecurely(renamedPage ?? page) };
-
     try {
     try {
       // global notification
       // global notification
       await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page, req.user, {
       await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page, req.user, {

+ 20 - 9
packages/app/src/server/service/page.ts

@@ -166,10 +166,13 @@ class PageService {
       }
       }
     });
     });
 
 
+    // Change the parameter of the function into the pages
     // rename
     // rename
-    this.pageEvent.on('rename', async(page, user) => {
+    this.pageEvent.on('rename', async(page, descendantPages, user) => {
+      const isRecursively = descendantPages != null;
+      const action = isRecursively ? SUPPORTED_ACTION_TYPE.ACTION_PAGE_RECURSIVELY_RENAME : SUPPORTED_ACTION_TYPE.ACTION_PAGE_RENAME;
       try {
       try {
-        await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_RENAME);
+        await this.createAndSendNotifications(page, user, action, descendantPages);
       }
       }
       catch (err) {
       catch (err) {
         logger.error(err);
         logger.error(err);
@@ -537,13 +540,12 @@ class PageService {
       update.updatedAt = new Date();
       update.updatedAt = new Date();
     }
     }
     const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
     const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
-
+    this.pageEvent.emit('rename', page, null, user);
     // create page redirect
     // create page redirect
     if (options.createRedirectPage) {
     if (options.createRedirectPage) {
       const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
       const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
       await PageRedirect.create({ fromPath: page.path, toPath: newPagePath });
       await PageRedirect.create({ fromPath: page.path, toPath: newPagePath });
     }
     }
-    this.pageEvent.emit('rename', page, user);
 
 
     // Set to Sub
     // Set to Sub
     const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(pageOpId, PageActionStage.Sub);
     const pageOp = await PageOperation.findByIdAndUpdatePageActionStage(pageOpId, PageActionStage.Sub);
@@ -582,6 +584,7 @@ class PageService {
     }
     }
 
 
     await PageOperation.findByIdAndDelete(pageOpId);
     await PageOperation.findByIdAndDelete(pageOpId);
+
   }
   }
 
 
   private isRenamingToUnderTarget(fromPath: string, toPath: string): boolean {
   private isRenamingToUnderTarget(fromPath: string, toPath: string): boolean {
@@ -682,7 +685,7 @@ class PageService {
       await PageRedirect.create({ fromPath: page.path, toPath: newPagePath });
       await PageRedirect.create({ fromPath: page.path, toPath: newPagePath });
     }
     }
 
 
-    this.pageEvent.emit('rename', page, user);
+    this.pageEvent.emit('rename', page, null, user);
 
 
     return renamedPage;
     return renamedPage;
   }
   }
@@ -837,6 +840,7 @@ class PageService {
           await renameDescendants(
           await renameDescendants(
             batch, user, options, pathRegExp, newPagePathPrefix, shouldUseV4Process,
             batch, user, options, pathRegExp, newPagePathPrefix, shouldUseV4Process,
           );
           );
+          pageEvent.emit('rename', targetPage, batch, user);
           logger.debug(`Renaming pages progressing: (count=${count})`);
           logger.debug(`Renaming pages progressing: (count=${count})`);
         }
         }
         catch (err) {
         catch (err) {
@@ -2230,7 +2234,7 @@ class PageService {
     return shortBodiesMap;
     return shortBodiesMap;
   }
   }
 
 
-  private async createAndSendNotifications(page, user, action) {
+  private async createAndSendNotifications(page: PageDocument, user: any, action: any, descendantPages?: PageDocument[]) {
     const { activityService, inAppNotificationService } = this.crowi;
     const { activityService, inAppNotificationService } = this.crowi;
 
 
     const snapshot = stringifySnapshot(page);
     const snapshot = stringifySnapshot(page);
@@ -2243,10 +2247,17 @@ class PageService {
       action,
       action,
     };
     };
     const activity = await activityService.createByParameters(parameters);
     const activity = await activityService.createByParameters(parameters);
-
     // Get user to be notified
     // Get user to be notified
-    const targetUsers = await activity.getNotificationTargetUsers();
-
+    let targetUsers = await activity.getNotificationTargetUsers();
+    if (descendantPages !== undefined && descendantPages.length > 0) {
+      const User = this.crowi.model('User');
+      const targetDescendantsUsers = await Subscription.getSubscriptions(descendantPages);
+      const descendantsUsers = targetDescendantsUsers.filter(item => (item.toString() !== user._id.toString()));
+      targetUsers = targetUsers.concat(await User.find({
+        _id: { $in: descendantsUsers },
+        status: User.STATUS_ACTIVE,
+      }).distinct('_id'));
+    }
     // Create and send notifications
     // Create and send notifications
     await inAppNotificationService.upsertByActivity(targetUsers, activity, snapshot);
     await inAppNotificationService.upsertByActivity(targetUsers, activity, snapshot);
     await inAppNotificationService.emitSocketIo(targetUsers);
     await inAppNotificationService.emitSocketIo(targetUsers);

+ 4 - 4
packages/app/test/integration/service/page.test.js

@@ -350,7 +350,7 @@ describe('PageService', () => {
 
 
         expect(xssSpy).toHaveBeenCalled();
         expect(xssSpy).toHaveBeenCalled();
 
 
-        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename1, testUser2);
+        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename1, null, testUser2);
 
 
         expect(resultPage.path).toBe('/renamed1');
         expect(resultPage.path).toBe('/renamed1');
         expect(resultPage.updatedAt).toEqual(parentForRename1.updatedAt);
         expect(resultPage.updatedAt).toEqual(parentForRename1.updatedAt);
@@ -363,7 +363,7 @@ describe('PageService', () => {
 
 
         expect(xssSpy).toHaveBeenCalled();
         expect(xssSpy).toHaveBeenCalled();
 
 
-        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename2, testUser2);
+        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename2, null, testUser2);
 
 
         expect(resultPage.path).toBe('/renamed2');
         expect(resultPage.path).toBe('/renamed2');
         expect(resultPage.updatedAt).toEqual(dateToUse);
         expect(resultPage.updatedAt).toEqual(dateToUse);
@@ -375,7 +375,7 @@ describe('PageService', () => {
         const resultPage = await crowi.pageService.renamePage(parentForRename3, '/renamed3', testUser2, { createRedirectPage: true });
         const resultPage = await crowi.pageService.renamePage(parentForRename3, '/renamed3', testUser2, { createRedirectPage: true });
 
 
         expect(xssSpy).toHaveBeenCalled();
         expect(xssSpy).toHaveBeenCalled();
-        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename3, testUser2);
+        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename3, null, testUser2);
 
 
         expect(resultPage.path).toBe('/renamed3');
         expect(resultPage.path).toBe('/renamed3');
         expect(resultPage.updatedAt).toEqual(parentForRename3.updatedAt);
         expect(resultPage.updatedAt).toEqual(parentForRename3.updatedAt);
@@ -388,7 +388,7 @@ describe('PageService', () => {
 
 
         expect(xssSpy).toHaveBeenCalled();
         expect(xssSpy).toHaveBeenCalled();
         expect(renameDescendantsWithStreamSpy).toHaveBeenCalled();
         expect(renameDescendantsWithStreamSpy).toHaveBeenCalled();
-        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename4, testUser2);
+        expect(pageEventSpy).toHaveBeenCalledWith('rename', parentForRename4, null, testUser2);
 
 
         expect(resultPage.path).toBe('/renamed4');
         expect(resultPage.path).toBe('/renamed4');
         expect(resultPage.updatedAt).toEqual(parentForRename4.updatedAt);
         expect(resultPage.updatedAt).toEqual(parentForRename4.updatedAt);