فهرست منبع

Merge branch 'master' of https://github.com/weseek/growi into feat/auditlog

Shun Miyazawa 4 سال پیش
والد
کامیت
bd123fd2f3

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

@@ -115,6 +115,10 @@ const InAppNotificationElm: FC<Props> = (props: Props) => {
       actionMsg = 'completely deleted';
       actionIcon = 'icon-fire';
       break;
+    case 'PAGE_REVERT':
+      actionMsg = 'reverted';
+      actionIcon = 'icon-action-undo';
+      break;
     case 'COMMENT_CREATE':
       actionMsg = 'commented on';
       actionIcon = 'icon-bubble';

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

@@ -0,0 +1,46 @@
+// Model
+const MODEL_PAGE = 'Page';
+const MODEL_COMMENT = 'Comment';
+
+// Action
+const ACTION_PAGE_LIKE = 'PAGE_LIKE';
+const ACTION_PAGE_BOOKMARK = 'PAGE_BOOKMARK';
+const ACTION_PAGE_UPDATE = 'PAGE_UPDATE';
+const ACTION_PAGE_RENAME = 'PAGE_RENAME';
+const ACTION_PAGE_DUPLICATE = 'PAGE_DUPLICATE';
+const ACTION_PAGE_DELETE = 'PAGE_DELETE';
+const ACTION_PAGE_DELETE_COMPLETELY = 'PAGE_DELETE_COMPLETELY';
+const ACTION_PAGE_REVERT = 'PAGE_REVERT';
+const ACTION_COMMENT_CREATE = 'COMMENT_CREATE';
+const ACTION_COMMENT_UPDATE = 'COMMENT_UPDATE';
+
+
+export const SUPPORTED_TARGET_MODEL_TYPE = {
+  MODEL_PAGE,
+} as const;
+
+export const SUPPORTED_EVENT_MODEL_TYPE = {
+  MODEL_COMMENT,
+} as const;
+
+export const SUPPORTED_ACTION_TYPE = {
+  ACTION_PAGE_LIKE,
+  ACTION_PAGE_BOOKMARK,
+  ACTION_PAGE_UPDATE,
+  ACTION_PAGE_RENAME,
+  ACTION_PAGE_DUPLICATE,
+  ACTION_PAGE_DELETE,
+  ACTION_PAGE_DELETE_COMPLETELY,
+  ACTION_PAGE_REVERT,
+  ACTION_COMMENT_CREATE,
+  ACTION_COMMENT_UPDATE,
+} as const;
+
+
+export const AllSupportedTargetModelType = Object.values(SUPPORTED_TARGET_MODEL_TYPE);
+export const AllSupportedEventModelType = Object.values(SUPPORTED_EVENT_MODEL_TYPE);
+export const AllSupportedActionType = Object.values(SUPPORTED_ACTION_TYPE);
+
+// type supportedTargetModelType = typeof SUPPORTED_TARGET_MODEL_NAMES[keyof typeof SUPPORTED_TARGET_MODEL_NAMES];
+// type supportedEventModelType = typeof SUPPORTED_EVENT_MODEL_NAMES[keyof typeof SUPPORTED_EVENT_MODEL_NAMES];
+// type supportedActionType = typeof SUPPORTED_ACTION_NAMES[keyof typeof SUPPORTED_ACTION_NAMES];

+ 6 - 8
packages/app/src/server/models/activity.ts

@@ -1,19 +1,17 @@
+import { getOrCreateModel, getModelSafely } from '@growi/core';
 import {
   Types, Document, Model, Schema,
 } from 'mongoose';
 
-import { getOrCreateModel, getModelSafely } from '@growi/core';
-import loggerFactory from '../../utils/logger';
-
+import { AllSupportedTargetModelType, AllSupportedEventModelType, AllSupportedActionType } from '~/interfaces/activity';
 
-import ActivityDefine from '../util/activityDefine';
+import loggerFactory from '../../utils/logger';
 import activityEvent from '../events/activity';
 
 import Subscription from './subscription';
 
 const logger = loggerFactory('growi:models:activity');
 
-
 export interface ActivityDocument extends Document {
   _id: Types.ObjectId
   user: Types.ObjectId | any
@@ -40,7 +38,7 @@ const activitySchema = new Schema<ActivityDocument, ActivityModel>({
   targetModel: {
     type: String,
     require: true,
-    enum: ActivityDefine.getSupportTargetModelNames(),
+    enum: AllSupportedTargetModelType,
   },
   target: {
     type: Schema.Types.ObjectId,
@@ -50,7 +48,7 @@ const activitySchema = new Schema<ActivityDocument, ActivityModel>({
   action: {
     type: String,
     require: true,
-    enum: ActivityDefine.getSupportActionNames(),
+    enum: AllSupportedActionType,
   },
   event: {
     type: Schema.Types.ObjectId,
@@ -58,7 +56,7 @@ const activitySchema = new Schema<ActivityDocument, ActivityModel>({
   },
   eventModel: {
     type: String,
-    enum: ActivityDefine.getSupportEventModelNames(),
+    enum: AllSupportedEventModelType,
   },
 }, {
   timestamps: true,

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

@@ -1,13 +1,14 @@
+import { getOrCreateModel } from '@growi/core';
 import {
   Types, Document, Schema, Model,
 } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
-import { getOrCreateModel } from '@growi/core';
+import { AllSupportedTargetModelType, AllSupportedActionType } from '~/interfaces/activity';
+import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
+
 import { ActivityDocument } from './activity';
-import ActivityDefine from '../util/activityDefine';
 
-import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
 
 const { STATUS_UNREAD, STATUS_UNOPENED, STATUS_OPENED } = InAppNotificationStatuses;
 
@@ -45,7 +46,7 @@ const inAppNotificationSchema = new Schema<InAppNotificationDocument, InAppNotif
   targetModel: {
     type: String,
     require: true,
-    enum: ActivityDefine.getSupportTargetModelNames(),
+    enum: AllSupportedTargetModelType,
   },
   target: {
     type: Schema.Types.ObjectId,
@@ -55,7 +56,7 @@ const inAppNotificationSchema = new Schema<InAppNotificationDocument, InAppNotif
   action: {
     type: String,
     require: true,
-    enum: ActivityDefine.getSupportActionNames(),
+    enum: AllSupportedActionType,
   },
   activities: [
     {

+ 3 - 4
packages/app/src/server/models/subscription.ts

@@ -1,12 +1,11 @@
+import { getOrCreateModel } from '@growi/core';
 import {
   Types, Document, Model, Schema,
 } from 'mongoose';
 
-import { getOrCreateModel } from '@growi/core';
-
+import { AllSupportedTargetModelType } from '~/interfaces/activity';
 import { SubscriptionStatusType, AllSubscriptionStatusType } from '~/interfaces/subscription';
 
-import ActivityDefine from '../util/activityDefine';
 
 export interface ISubscription {
   user: Types.ObjectId
@@ -39,7 +38,7 @@ const subscriptionSchema = new Schema<SubscriptionDocument, SubscriptionModel>({
   targetModel: {
     type: String,
     require: true,
-    enum: ActivityDefine.getSupportTargetModelNames(),
+    enum: AllSupportedTargetModelType,
   },
   target: {
     type: Schema.Types.ObjectId,

+ 9 - 7
packages/app/src/server/service/comment.ts

@@ -1,10 +1,12 @@
-import { Types } from 'mongoose';
 import { getModelSafely } from '@growi/core';
+import { Types } from 'mongoose';
+
+import { SUPPORTED_TARGET_MODEL_TYPE, SUPPORTED_EVENT_MODEL_TYPE, SUPPORTED_ACTION_TYPE } from '~/interfaces/activity';
+import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
+
 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');
 
@@ -43,7 +45,7 @@ class CommentService {
           return;
         }
 
-        const activity = await this.createActivity(savedComment, ActivityDefine.ACTION_COMMENT_CREATE);
+        const activity = await this.createActivity(savedComment, SUPPORTED_ACTION_TYPE.ACTION_COMMENT_CREATE);
         await this.createAndSendNotifications(activity, page);
       }
       catch (err) {
@@ -56,7 +58,7 @@ class CommentService {
     this.commentEvent.on('update', async(updatedComment) => {
       try {
         this.commentEvent.onUpdate();
-        await this.createActivity(updatedComment, ActivityDefine.ACTION_COMMENT_UPDATE);
+        await this.createActivity(updatedComment, SUPPORTED_ACTION_TYPE.ACTION_COMMENT_UPDATE);
       }
       catch (err) {
         logger.error('Error occurred while handling the comment update event:\n', err);
@@ -80,9 +82,9 @@ class CommentService {
   private createActivity = async function(comment, action) {
     const parameters = {
       user: comment.creator,
-      targetModel: ActivityDefine.MODEL_PAGE,
+      targetModel: SUPPORTED_TARGET_MODEL_TYPE.MODEL_PAGE,
       target: comment.page,
-      eventModel: ActivityDefine.MODEL_COMMENT,
+      eventModel: SUPPORTED_EVENT_MODEL_TYPE.MODEL_COMMENT,
       event: comment._id,
       action,
     };

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

@@ -6,6 +6,7 @@ import escapeStringRegexp from 'escape-string-regexp';
 import mongoose, { ObjectId, QueryCursor } from 'mongoose';
 import streamToPromise from 'stream-to-promise';
 
+import { SUPPORTED_TARGET_MODEL_TYPE, SUPPORTED_ACTION_TYPE } from '~/interfaces/activity';
 import { Ref } from '~/interfaces/common';
 import { HasObjectId } from '~/interfaces/has-object-id';
 import {
@@ -30,7 +31,6 @@ import PageOperation, { PageActionStage, PageActionType } from '../models/page-o
 import { PageRedirectModel } from '../models/page-redirect';
 import { serializePageSecurely } from '../models/serializers/page-serializer';
 import Subscription from '../models/subscription';
-import ActivityDefine from '../util/activityDefine';
 
 const debug = require('debug')('growi:services:page');
 
@@ -157,7 +157,7 @@ class PageService {
       this.pageEvent.onUpdate();
 
       try {
-        await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_UPDATE);
+        await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_UPDATE);
       }
       catch (err) {
         logger.error(err);
@@ -167,7 +167,7 @@ class PageService {
     // rename
     this.pageEvent.on('rename', async(page, user) => {
       try {
-        await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_RENAME);
+        await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_RENAME);
       }
       catch (err) {
         logger.error(err);
@@ -177,7 +177,7 @@ class PageService {
     // duplicate
     this.pageEvent.on('duplicate', async(page, user) => {
       try {
-        await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DUPLICATE);
+        await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_DUPLICATE);
       }
       catch (err) {
         logger.error(err);
@@ -187,7 +187,7 @@ class PageService {
     // delete
     this.pageEvent.on('delete', async(page, user) => {
       try {
-        await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DELETE);
+        await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_DELETE);
       }
       catch (err) {
         logger.error(err);
@@ -197,7 +197,17 @@ class PageService {
     // delete completely
     this.pageEvent.on('deleteCompletely', async(page, user) => {
       try {
-        await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DELETE_COMPLETELY);
+        await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_DELETE_COMPLETELY);
+      }
+      catch (err) {
+        logger.error(err);
+      }
+    });
+
+    // revert
+    this.pageEvent.on('revert', async(page, user) => {
+      try {
+        await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_REVERT);
       }
       catch (err) {
         logger.error(err);
@@ -207,7 +217,7 @@ class PageService {
     // likes
     this.pageEvent.on('like', async(page, user) => {
       try {
-        await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_LIKE);
+        await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_LIKE);
       }
       catch (err) {
         logger.error(err);
@@ -217,7 +227,7 @@ class PageService {
     // bookmark
     this.pageEvent.on('bookmark', async(page, user) => {
       try {
-        await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_BOOKMARK);
+        await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_BOOKMARK);
       }
       catch (err) {
         logger.error(err);
@@ -1906,6 +1916,8 @@ class PageService {
     }, { new: true });
     await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
 
+    this.pageEvent.emit('revert', page, user);
+
     if (!isRecursively) {
       await this.updateDescendantCountOfAncestors(parent._id, 1, true);
     }
@@ -2005,6 +2017,8 @@ class PageService {
     }, { new: true });
     await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
 
+    this.pageEvent.emit('revert', page, user);
+
     return updatedPage;
   }
 
@@ -2223,7 +2237,7 @@ class PageService {
     // Create activity
     const parameters = {
       user: user._id,
-      targetModel: ActivityDefine.MODEL_PAGE,
+      targetModel: SUPPORTED_TARGET_MODEL_TYPE.MODEL_PAGE,
       target: page,
       action,
     };

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

@@ -1,57 +0,0 @@
-// TargetModel
-const MODEL_PAGE = 'Page';
-const MODEL_COMMENT = 'Comment';
-
-// Activity
-const ACTION_PAGE_LIKE = 'PAGE_LIKE';
-const ACTION_PAGE_BOOKMARK = 'PAGE_BOOKMARK';
-const ACTION_PAGE_UPDATE = 'PAGE_UPDATE';
-const ACTION_PAGE_RENAME = 'PAGE_RENAME';
-const ACTION_PAGE_DUPLICATE = 'PAGE_DUPLICATE';
-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';
-
-const getSupportTargetModelNames = () => {
-  return [MODEL_PAGE];
-};
-
-const getSupportEventModelNames = () => {
-  return [MODEL_COMMENT];
-};
-
-const getSupportActionNames = () => {
-  return [
-    ACTION_PAGE_LIKE,
-    ACTION_PAGE_BOOKMARK,
-    ACTION_PAGE_UPDATE,
-    ACTION_PAGE_RENAME,
-    ACTION_PAGE_DUPLICATE,
-    ACTION_PAGE_DELETE,
-    ACTION_PAGE_DELETE_COMPLETELY,
-    ACTION_COMMENT_CREATE,
-    ACTION_COMMENT_UPDATE,
-  ];
-};
-
-const activityDefine = {
-  MODEL_PAGE,
-  MODEL_COMMENT,
-
-  ACTION_PAGE_LIKE,
-  ACTION_PAGE_BOOKMARK,
-  ACTION_PAGE_UPDATE,
-  ACTION_PAGE_RENAME,
-  ACTION_PAGE_DUPLICATE,
-  ACTION_PAGE_DELETE,
-  ACTION_PAGE_DELETE_COMPLETELY,
-  ACTION_COMMENT_CREATE,
-  ACTION_COMMENT_UPDATE,
-
-  getSupportTargetModelNames,
-  getSupportEventModelNames,
-  getSupportActionNames,
-};
-
-export default activityDefine;

+ 12 - 15
packages/app/src/styles/_page_list.scss

@@ -49,15 +49,23 @@ body .page-list {
         margin-right: 2px;
       }
 
+      .footstamp-icon {
+        svg {
+          width: 14px;
+          height: 14px;
+        }
+      }
       .seen-users-count {
-        &.strength-1 {
+        &.strength-0,
+        &.strength-1,
+        &.strength-2 {
           font-weight: bold;
         }
-        &.strength-2 {
+        &.strength-3 {
           font-weight: normal;
         }
-        &.strength-3 {
-          opacity: 0.6;
+        &.strength-4 {
+          opacity: 0.5;
         }
       }
     }
@@ -78,17 +86,6 @@ body .page-list {
     .list-group-item {
       min-height: 136px;
 
-      .page-list-meta {
-        .meta-icon {
-          width: 14px;
-          height: 14px;
-          margin-right: 14px;
-        }
-        .footstamp-icon {
-          margin-right: 2px;
-        }
-      }
-
       .page-list-updated-at {
         font-size: 12px;
       }

+ 1 - 1
packages/plugin-lsx/src/client/js/components/Lsx.jsx

@@ -60,7 +60,7 @@ export class Lsx extends React.Component {
     try {
       const res = await this.props.appContainer.apiGet('/plugins/lsx', { pagePath, options: lsxContext.options });
 
-      lsxContext.activeUsersCount = res.activeUsersCount;
+      lsxContext.toppageViewersCount = res.toppageViewersCount;
 
       if (res.ok) {
         const nodeTree = this.generatePageNodeTree(pagePath, res.pages);

+ 2 - 2
packages/plugin-lsx/src/client/js/components/LsxPageList/LsxPage.jsx

@@ -79,7 +79,7 @@ export class LsxPage extends React.Component {
 
   render() {
     const pageNode = this.props.pageNode;
-    const { activeUsersCount } = this.props.lsxContext;
+    const { toppageViewersCount } = this.props.lsxContext;
 
     // create PagePath element
     let pagePathNode = <PagePathWrapper pagePath={pageNode.pagePath} isExists={this.state.isExists} />;
@@ -88,7 +88,7 @@ export class LsxPage extends React.Component {
     }
 
     // create PageListMeta element
-    const pageListMeta = (this.state.isExists) ? <PageListMeta page={pageNode.page} activeUsersCount={activeUsersCount} /> : '';
+    const pageListMeta = (this.state.isExists) ? <PageListMeta page={pageNode.page} basisViewersCount={toppageViewersCount} /> : '';
 
     return (
       <li className="page-list-li">

+ 11 - 4
packages/plugin-lsx/src/server/routes/lsx.js

@@ -208,10 +208,17 @@ module.exports = (crowi, app) => {
 
     const builder = await generateBaseQueryBuilder(pagePath, user);
 
-    // count active users
-    let activeUsersCount;
+    // count viewers of `/`
+    let toppageViewersCount;
     try {
-      activeUsersCount = await User.countListByStatus(User.STATUS_ACTIVE);
+      const aggRes = await Page.aggregate([
+        { $match: { path: '/' } },
+        { $project: { count: { $size: '$seenUsers' } } },
+      ]);
+
+      toppageViewersCount = aggRes.length > 0
+        ? aggRes[0].count
+        : 1;
     }
     catch (error) {
       return res.json(ApiResponse.error(error));
@@ -237,7 +244,7 @@ module.exports = (crowi, app) => {
       query = Lsx.addSortCondition(query, pagePath, options.sort, options.reverse);
 
       const pages = await query.exec();
-      res.json(ApiResponse.success({ pages, activeUsersCount }));
+      res.json(ApiResponse.success({ pages, toppageViewersCount }));
     }
     catch (error) {
       return res.json(ApiResponse.error(error));

+ 14 - 14
packages/ui/src/components/PagePath/PageListMeta.tsx

@@ -12,38 +12,38 @@ const { checkTemplatePath } = templateChecker;
 
 
 const SEEN_USERS_HIDE_THRES__ACTIVE_USERS_COUNT = 5;
-const MIN_STRENGTH_LEVEL = -3;
+const MAX_STRENGTH_LEVEL = 4;
 
 type SeenUsersCountProps = {
   count: number,
-  activeUsersCount?: number,
+  basisViewersCount?: number,
   shouldSpaceOutIcon?: boolean,
 }
 
 const SeenUsersCount = (props: SeenUsersCountProps): JSX.Element => {
 
-  const { count, shouldSpaceOutIcon, activeUsersCount } = props;
+  const { count, shouldSpaceOutIcon, basisViewersCount } = props;
 
   if (count === 0) {
     return <></>;
   }
 
-  if (activeUsersCount != null && activeUsersCount <= SEEN_USERS_HIDE_THRES__ACTIVE_USERS_COUNT) {
+  if (basisViewersCount != null && basisViewersCount <= SEEN_USERS_HIDE_THRES__ACTIVE_USERS_COUNT) {
     return <></>;
   }
 
-  const strengthLevel = Math.log(count / (activeUsersCount ?? count)); // Max: 0
+  const strengthLevel = Math.ceil(
+    Math.min(0, Math.log(count / (basisViewersCount ?? count))) // Max: 0
+    * 2 * -1,
+  );
 
-  if (strengthLevel <= MIN_STRENGTH_LEVEL) {
+  if (strengthLevel > MAX_STRENGTH_LEVEL) {
     return <></>;
   }
 
-  assert(strengthLevel > MIN_STRENGTH_LEVEL); // [0, MIN_STRENGTH_LEVEL)
+  assert(strengthLevel >= 0 && strengthLevel <= MAX_STRENGTH_LEVEL); // [0, MAX_STRENGTH_LEVEL)
 
-  let strengthClass = '';
-  if (strengthLevel < 0) {
-    strengthClass = `strength-${Math.ceil(strengthLevel * -1)}`; // strength-{0, 1, 2, 3}
-  }
+  const strengthClass = `strength-${strengthLevel}`; // strength-{0, 1, 2, 3, 4}
 
   return (
     <span className={`seen-users-count ${shouldSpaceOutIcon ? 'mr-3' : ''} ${strengthClass}`}>
@@ -60,12 +60,12 @@ type PageListMetaProps = {
   likerCount?: number,
   bookmarkCount?: number,
   shouldSpaceOutIcon?: boolean,
-  activeUsersCount?: number,
+  basisViewersCount?: number,
 }
 
 export const PageListMeta: FC<PageListMetaProps> = (props: PageListMetaProps) => {
 
-  const { page, shouldSpaceOutIcon, activeUsersCount } = props;
+  const { page, shouldSpaceOutIcon, basisViewersCount } = props;
 
   // top check
   let topLabel;
@@ -103,7 +103,7 @@ export const PageListMeta: FC<PageListMetaProps> = (props: PageListMetaProps) =>
     <span className="page-list-meta">
       {topLabel}
       {templateLabel}
-      <SeenUsersCount count={page.seenUsers.length} activeUsersCount={activeUsersCount} />
+      <SeenUsersCount count={page.seenUsers.length} basisViewersCount={basisViewersCount} />
       {commentCount}
       {likerCount}
       {locked}