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

Merge pull request #4924 from weseek/feat/create-page-snapshot

Feat/create page snapshot
Shun Miyazawa 4 лет назад
Родитель
Сommit
a21b099aa4

+ 17 - 36
packages/app/src/components/InAppNotification/InAppNotificationElm.tsx

@@ -1,22 +1,20 @@
-import React, { useCallback } from 'react';
+import React from 'react';
 
-import { UserPicture, PagePathLabel } from '@growi/ui';
+import { UserPicture } from '@growi/ui';
 import { IInAppNotification } from '~/interfaces/in-app-notification';
 import { HasObjectId } from '~/interfaces/has-object-id';
-import { apiv3Post } from '~/client/util/apiv3-client';
-import FormattedDistanceDate from '../FormattedDistanceDate';
 
+// Change the display for each targetmodel
+import PageModelNotification from './PageNotification/PageModelNotification';
 
 interface Props {
   notification: IInAppNotification & HasObjectId
 }
 
-// TODO 81946 Return to not nullable
-const InAppNotificationElm = (props: Props): JSX.Element | null => {
+const InAppNotificationElm = (props: Props): JSX.Element => {
 
   const { notification } = props;
 
-
   const getActionUsers = () => {
     const latestActionUsers = notification.actionUsers.slice(0, 3);
     const latestUsers = latestActionUsers.map((user) => {
@@ -58,22 +56,8 @@ const InAppNotificationElm = (props: Props): JSX.Element | null => {
     );
   };
 
-  const notificationClickHandler = useCallback(() => {
-    // set notification status "OPEND"
-    apiv3Post('/in-app-notification/open', { id: notification._id });
-
-    // jump to target page
-    window.location.href = notification.target.path;
-  }, []);
-
   const actionUsers = getActionUsers();
 
-  // TODO 81946 Return to not nullable
-  const pagePath = { path: props.notification.target?.path };
-  if (pagePath.path == null) {
-    return null;
-  }
-
   const actionType: string = notification.action;
   let actionMsg: string;
   let actionIcon: string;
@@ -99,6 +83,10 @@ const InAppNotificationElm = (props: Props): JSX.Element | null => {
       actionMsg = 'deleted';
       actionIcon = 'icon-trash';
       break;
+    case 'PAGE_DELETE_COMPLETELY':
+      actionMsg = 'completely deleted';
+      actionIcon = 'icon-fire';
+      break;
     case 'COMMENT_CREATE':
       actionMsg = 'commented on';
       actionIcon = 'icon-bubble';
@@ -108,27 +96,20 @@ const InAppNotificationElm = (props: Props): JSX.Element | null => {
       actionIcon = '';
   }
 
-
   return (
     <div className="dropdown-item d-flex flex-row mb-3">
       <div className="p-2 mr-2 d-flex align-items-center">
         <span className={`${notification.status === 'UNOPENED' ? 'grw-unopend-notification' : 'ml-2'} rounded-circle mr-3`}></span>
         {renderActionUserPictures()}
       </div>
-      <div className="p-2">
-        <div onClick={notificationClickHandler}>
-          <div>
-            <b>{actionUsers}</b> {actionMsg} <PagePathLabel page={pagePath} />
-          </div>
-          <i className={`${actionIcon} mr-2`} />
-          <FormattedDistanceDate
-            id={notification._id}
-            date={notification.createdAt}
-            isShowTooltip={false}
-            differenceForAvoidingFormat={Number.POSITIVE_INFINITY}
-          />
-        </div>
-      </div>
+      {notification.targetModel === 'Page' && (
+        <PageModelNotification
+          notification={notification}
+          actionMsg={actionMsg}
+          actionIcon={actionIcon}
+          actionUsers={actionUsers}
+        />
+      )}
     </div>
   );
 };

+ 53 - 0
packages/app/src/components/InAppNotification/PageNotification/PageModelNotification.tsx

@@ -0,0 +1,53 @@
+import React, { FC, useCallback } from 'react';
+import { PagePathLabel } from '@growi/ui';
+import { apiv3Post } from '~/client/util/apiv3-client';
+import { parseSnapshot } from '../../../models/serializers/in-app-notification-snapshot/page';
+import { IInAppNotification } from '~/interfaces/in-app-notification';
+import { HasObjectId } from '~/interfaces/has-object-id';
+import FormattedDistanceDate from '../../FormattedDistanceDate';
+
+interface Props {
+  notification: IInAppNotification & HasObjectId
+  actionMsg: string
+  actionIcon: string
+  actionUsers: string
+}
+
+const PageModelNotification: FC<Props> = (props: Props) => {
+  const {
+    notification, actionMsg, actionIcon, actionUsers,
+  } = props;
+
+  const snapshot = parseSnapshot(notification.snapshot);
+  const pagePath = { path: snapshot.path };
+
+  const notificationClickHandler = useCallback(() => {
+    // set notification status "OPEND"
+    apiv3Post('/in-app-notification/open', { id: notification._id });
+
+    // jump to target page
+    const targetPagePath = notification.target?.path;
+    if (targetPagePath != null) {
+      window.location.href = targetPagePath;
+    }
+  }, []);
+
+  return (
+    <div className="p-2">
+      <div onClick={notificationClickHandler}>
+        <div>
+          <b>{actionUsers}</b> {actionMsg} <PagePathLabel page={pagePath} />
+        </div>
+        <i className={`${actionIcon} mr-2`} />
+        <FormattedDistanceDate
+          id={notification._id}
+          date={notification.createdAt}
+          isShowTooltip={false}
+          differenceForAvoidingFormat={Number.POSITIVE_INFINITY}
+        />
+      </div>
+    </div>
+  );
+};
+
+export default PageModelNotification;

+ 1 - 0
packages/app/src/interfaces/in-app-notification.ts

@@ -16,6 +16,7 @@ export interface IInAppNotification {
   status: InAppNotificationStatuses
   actionUsers: IUser[]
   createdAt: Date
+  snapshot: string
 }
 
 /*

+ 18 - 0
packages/app/src/models/serializers/in-app-notification-snapshot/page.ts

@@ -0,0 +1,18 @@
+import { IUser } from '~/interfaces/user';
+import { IPage } from '~/interfaces/page';
+
+export interface IPageSnapshot {
+  path: string
+  creator: IUser
+}
+
+export const stringifySnapshot = (page: IPage): string => {
+  return JSON.stringify({
+    path: page.path,
+    creator: page.creator,
+  });
+};
+
+export const parseSnapshot = (snapshot: string): IPageSnapshot => {
+  return JSON.parse(snapshot);
+};

+ 5 - 0
packages/app/src/server/models/in-app-notification.ts

@@ -20,6 +20,7 @@ export interface InAppNotificationDocument extends Document {
   activities: ActivityDocument[]
   status: string
   createdAt: Date
+  snapshot: string
 }
 
 
@@ -73,6 +74,10 @@ const inAppNotificationSchema = new Schema<InAppNotificationDocument, InAppNotif
     type: Date,
     default: Date.now,
   },
+  snapshot: {
+    type: String,
+    require: true,
+  },
 });
 inAppNotificationSchema.plugin(mongoosePaginate);
 

+ 12 - 3
packages/app/src/server/service/comment.ts

@@ -4,6 +4,8 @@ import loggerFactory from '../../utils/logger';
 import ActivityDefine from '../util/activityDefine';
 import Crowi from '../crowi';
 
+import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+
 const logger = loggerFactory('growi:service:CommentService');
 
 class CommentService {
@@ -35,8 +37,14 @@ class CommentService {
         const Page = getModelSafely('Page') || require('../models/page')(this.crowi);
         await Page.updateCommentCount(savedComment.page);
 
+        const page = await Page.findById(savedComment.page);
+        if (page == null) {
+          logger.error('Page is not found');
+          return;
+        }
+
         const activity = await this.createActivity(savedComment, ActivityDefine.ACTION_COMMENT_CREATE);
-        await this.createAndSendNotifications(activity);
+        await this.createAndSendNotifications(activity, page);
       }
       catch (err) {
         logger.error('Error occurred while handling the comment create event:\n', err);
@@ -82,14 +90,15 @@ class CommentService {
     return activity;
   };
 
-  private createAndSendNotifications = async function(activity) {
+  private createAndSendNotifications = async function(activity, page) {
+    const snapshot = stringifySnapshot(page);
 
     // Get user to be notified
     let targetUsers: Types.ObjectId[] = [];
     targetUsers = await activity.getNotificationTargetUsers();
 
     // Create and send notifications
-    await this.inAppNotificationService.upsertByActivity(targetUsers, activity);
+    await this.inAppNotificationService.upsertByActivity(targetUsers, activity, snapshot);
     await this.inAppNotificationService.emitSocketIo(targetUsers);
   };
 

+ 3 - 2
packages/app/src/server/service/in-app-notification.ts

@@ -52,7 +52,7 @@ export default class InAppNotificationService {
   }
 
   upsertByActivity = async function(
-      users: Types.ObjectId[], activity: ActivityDocument, createdAt?: Date | null,
+      users: Types.ObjectId[], activity: ActivityDocument, snapshot: string, createdAt?: Date | null,
   ): Promise<void> {
     const {
       _id: activityId, targetModel, target, action,
@@ -61,7 +61,7 @@ export default class InAppNotificationService {
     const lastWeek = subDays(now, 7);
     const operations = users.map((user) => {
       const filter = {
-        user, target, action, createdAt: { $gt: lastWeek },
+        user, target, action, createdAt: { $gt: lastWeek }, snapshot,
       };
       const parameters = {
         user,
@@ -70,6 +70,7 @@ export default class InAppNotificationService {
         action,
         status: STATUS_UNREAD,
         createdAt: now,
+        snapshot,
         $addToSet: { activities: activityId },
       };
       return {

+ 16 - 4
packages/app/src/server/service/page.js

@@ -1,8 +1,9 @@
 import { pagePathUtils } from '@growi/core';
-import isThisHour from 'date-fns/isThisHour/index.js';
 import loggerFactory from '~/utils/logger';
 import ActivityDefine from '../util/activityDefine';
 
+import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+
 const mongoose = require('mongoose');
 const escapeStringRegexp = require('escape-string-regexp');
 const streamToPromise = require('stream-to-promise');
@@ -67,6 +68,16 @@ class PageService {
       }
     });
 
+    // delete completely
+    this.pageEvent.on('deleteCompletely', async(page, user) => {
+      try {
+        await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DELETE_COMPLETELY);
+      }
+      catch (err) {
+        logger.error(err);
+      }
+    });
+
     // likes
     this.pageEvent.on('like', async(page, user) => {
       try {
@@ -611,7 +622,7 @@ class PageService {
       this.deleteCompletelyDescendantsWithStream(page, user, options);
     }
 
-    this.pageEvent.emit('delete', page, user); // update as renamed page
+    this.pageEvent.emit('deleteCompletely', page, user); // update as renamed page
 
     return;
   }
@@ -799,9 +810,10 @@ class PageService {
   }
 
   createAndSendNotifications = async function(page, user, action) {
-
     const { activityService, inAppNotificationService } = this.crowi;
 
+    const snapshot = stringifySnapshot(page);
+
     // Create activity
     const parameters = {
       user: user._id,
@@ -815,7 +827,7 @@ class PageService {
     const targetUsers = await activity.getNotificationTargetUsers();
 
     // Create and send notifications
-    await inAppNotificationService.upsertByActivity(targetUsers, activity);
+    await inAppNotificationService.upsertByActivity(targetUsers, activity, snapshot);
     await inAppNotificationService.emitSocketIo(targetUsers);
   };
 

+ 3 - 0
packages/app/src/server/util/activityDefine.ts

@@ -6,6 +6,7 @@ const ACTION_PAGE_BOOKMARK = 'PAGE_BOOKMARK';
 const ACTION_PAGE_UPDATE = 'PAGE_UPDATE';
 const ACTION_PAGE_RENAME = 'PAGE_RENAME';
 const ACTION_PAGE_DELETE = 'PAGE_DELETE';
+const ACTION_PAGE_DELETE_COMPLETELY = 'PAGE_DELETE_COMPLETELY';
 const ACTION_COMMENT_CREATE = 'COMMENT_CREATE';
 const ACTION_COMMENT_UPDATE = 'COMMENT_UPDATE';
 
@@ -24,6 +25,7 @@ const getSupportActionNames = () => {
     ACTION_PAGE_UPDATE,
     ACTION_PAGE_RENAME,
     ACTION_PAGE_DELETE,
+    ACTION_PAGE_DELETE_COMPLETELY,
     ACTION_COMMENT_CREATE,
     ACTION_COMMENT_UPDATE,
   ];
@@ -38,6 +40,7 @@ const activityDefine = {
   ACTION_PAGE_UPDATE,
   ACTION_PAGE_RENAME,
   ACTION_PAGE_DELETE,
+  ACTION_PAGE_DELETE_COMPLETELY,
   ACTION_COMMENT_CREATE,
   ACTION_COMMENT_UPDATE,
 

+ 2 - 2
packages/app/src/test/integration/service/page.test.js

@@ -723,7 +723,7 @@ describe('PageService', () => {
       expect(deleteCompletelyOperationSpy).toHaveBeenCalled();
       expect(deleteCompletelyDescendantsWithStreamSpy).not.toHaveBeenCalled();
 
-      expect(pageEventSpy).toHaveBeenCalledWith('delete', parentForDeleteCompletely, testUser2);
+      expect(pageEventSpy).toHaveBeenCalledWith('deleteCompletely', parentForDeleteCompletely, testUser2);
     });
 
 
@@ -733,7 +733,7 @@ describe('PageService', () => {
       expect(deleteCompletelyOperationSpy).toHaveBeenCalled();
       expect(deleteCompletelyDescendantsWithStreamSpy).toHaveBeenCalled();
 
-      expect(pageEventSpy).toHaveBeenCalledWith('delete', parentForDeleteCompletely, testUser2);
+      expect(pageEventSpy).toHaveBeenCalledWith('deleteCompletely', parentForDeleteCompletely, testUser2);
     });
   });