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

Merge pull request #10436 from growilabs/feat/172383-recent-activity-react-component

feat: React components for displaying Activity Log
Yuki Takei 5 месяцев назад
Родитель
Сommit
1b773bf63c

+ 41 - 0
apps/app/src/client/components/RecentActivity/ActivityListItem.tsx

@@ -0,0 +1,41 @@
+import type { ActivityWithPageTarget, SupportedActivityActionType } from '~/interfaces/activity';
+import { ActivityLogActions } from '~/interfaces/activity';
+
+import { PageListItemS } from '../PageList/PageListItemS';
+
+export const ActivityActionTranslationMap: Record<
+  SupportedActivityActionType,
+  string
+> = {
+  [ActivityLogActions.ACTION_PAGE_CREATE]: 'created a page',
+  [ActivityLogActions.ACTION_PAGE_UPDATE]: 'updated a page',
+  [ActivityLogActions.ACTION_PAGE_DELETE]: 'deleted a page',
+  [ActivityLogActions.ACTION_PAGE_RENAME]: 'renamed a page',
+  [ActivityLogActions.ACTION_PAGE_REVERT]: 'reverted a page',
+  [ActivityLogActions.ACTION_PAGE_DUPLICATE]: 'duplicated a page',
+  [ActivityLogActions.ACTION_COMMENT_CREATE]: 'posted a comment',
+  [ActivityLogActions.ACTION_COMMENT_UPDATE]: 'edited a comment',
+  [ActivityLogActions.ACTION_COMMENT_REMOVE]: 'deleted a comment',
+  [ActivityLogActions.ACTION_ATTACHMENT_ADD]: 'added an attachment',
+};
+
+const translateAction = (action: SupportedActivityActionType): string => {
+  return ActivityActionTranslationMap[action] || 'performed an unknown action';
+};
+
+
+export const ActivityListItem = ({ activity }: { activity: ActivityWithPageTarget }): JSX.Element => {
+  const username = activity.user?.username;
+  const action = activity.action as SupportedActivityActionType;
+  const date = new Date(activity.createdAt).toLocaleString();
+
+  return (
+    <div className="activity-row">
+      <p className="text-muted small mb-1">
+        {username} {translateAction(action)} on {date}
+      </p>
+
+      <PageListItemS page={activity.target} />
+    </div>
+  );
+};

+ 78 - 0
apps/app/src/client/components/RecentActivity/RecentActivity.tsx

@@ -0,0 +1,78 @@
+import React, {
+  useState, useCallback, useEffect, type JSX,
+} from 'react';
+
+import { toastError } from '~/client/util/toastr';
+import type { IActivityHasId, ActivityWithPageTarget } from '~/interfaces/activity';
+import { useSWRxRecentActivity } from '~/stores/recent-activity';
+import loggerFactory from '~/utils/logger';
+
+import PaginationWrapper from '../PaginationWrapper';
+
+import { ActivityListItem } from './ActivityListItem';
+
+
+const logger = loggerFactory('growi:RecentActivity');
+
+
+const hasPageTarget = (activity: IActivityHasId): activity is ActivityWithPageTarget => {
+  return activity.target != null
+        && typeof activity.target === 'object'
+        && '_id' in activity.target;
+};
+
+export const RecentActivity = (): JSX.Element => {
+  const [activities, setActivities] = useState<ActivityWithPageTarget[]>([]);
+  const [activePage, setActivePage] = useState(1);
+  const [limit] = useState(10);
+  const [offset, setOffset] = useState(0);
+
+  const { data: paginatedData, error } = useSWRxRecentActivity(limit, offset);
+
+  const handlePage = useCallback(async(selectedPage: number) => {
+    const newOffset = (selectedPage - 1) * limit;
+
+    setOffset(newOffset);
+    setActivePage(selectedPage);
+  }, [limit]);
+
+  useEffect(() => {
+    if (error) {
+      logger.error('Failed to fetch recent activity data', error);
+      toastError(error);
+      return;
+    }
+
+    if (paginatedData) {
+      const activitiesWithPages = paginatedData.docs
+        .filter(hasPageTarget);
+
+      setActivities(activitiesWithPages);
+    }
+  }, [paginatedData, error]);
+
+  const totalPageCount = paginatedData?.totalDocs || 0;
+
+
+  return (
+    <div className="page-list-container-activity">
+      <ul className="page-list-ul page-list-ul-flat mb-3">
+        {activities.map(activity => (
+          <li key={`recent-activity-view:${activity._id}`} className="mt-4">
+            <ActivityListItem activity={activity} />
+          </li>
+        ))}
+      </ul>
+
+      <PaginationWrapper
+        activePage={activePage}
+        changePage={handlePage}
+        totalItemsCount={totalPageCount}
+        pagingLimit={limit}
+        align="center"
+        size="sm"
+      />
+    </div>
+  );
+
+};

+ 4 - 0
apps/app/src/client/components/UsersHomepageFooter.tsx

@@ -2,6 +2,7 @@ import React, { useState, type JSX } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
+import { RecentActivity } from '~/client/components/RecentActivity/RecentActivity';
 import { RecentCreated } from '~/client/components/RecentCreated/RecentCreated';
 import { RecentCreated } from '~/client/components/RecentCreated/RecentCreated';
 import { useCurrentUser } from '~/stores-universal/context';
 import { useCurrentUser } from '~/stores-universal/context';
 
 
@@ -45,6 +46,9 @@ export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Elemen
         <div id="user-created-list" className={`page-list ${styles['page-list']}`}>
         <div id="user-created-list" className={`page-list ${styles['page-list']}`}>
           <RecentCreated userId={creatorId} />
           <RecentCreated userId={creatorId} />
         </div>
         </div>
+        <div id="user-created-list" className={`page-list ${styles['page-list']}`}>
+          <RecentActivity />
+        </div>
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 16 - 2
apps/app/src/interfaces/activity.ts

@@ -1,4 +1,10 @@
-import type { HasObjectId, IUser, Ref } from '@growi/core';
+import type {
+  HasObjectId,
+  IPageHasId,
+  IUser,
+  IUserHasId,
+  Ref,
+} from '@growi/core';
 import type { PaginateResult } from './mongoose-utils';
 import type { PaginateResult } from './mongoose-utils';
 
 
 // Model
 // Model
@@ -575,8 +581,10 @@ export const ActivityLogActions = {
   ACTION_PAGE_RENAME,
   ACTION_PAGE_RENAME,
   ACTION_PAGE_DUPLICATE,
   ACTION_PAGE_DUPLICATE,
   ACTION_PAGE_DELETE,
   ACTION_PAGE_DELETE,
+  ACTION_PAGE_REVERT,
   ACTION_COMMENT_CREATE,
   ACTION_COMMENT_CREATE,
   ACTION_COMMENT_UPDATE,
   ACTION_COMMENT_UPDATE,
+  ACTION_COMMENT_REMOVE,
   ACTION_ATTACHMENT_ADD,
   ACTION_ATTACHMENT_ADD,
 } as const;
 } as const;
 
 
@@ -657,7 +665,8 @@ export type SupportedActionType =
   (typeof SupportedAction)[keyof typeof SupportedAction];
   (typeof SupportedAction)[keyof typeof SupportedAction];
 export type SupportedActionCategoryType =
 export type SupportedActionCategoryType =
   (typeof SupportedActionCategory)[keyof typeof SupportedActionCategory];
   (typeof SupportedActionCategory)[keyof typeof SupportedActionCategory];
-
+export type SupportedActivityActionType =
+  (typeof ActivityLogActions)[keyof typeof ActivityLogActions];
 export type ISnapshot = Partial<Pick<IUser, 'username'>>;
 export type ISnapshot = Partial<Pick<IUser, 'username'>>;
 
 
 export type IActivity = {
 export type IActivity = {
@@ -673,6 +682,11 @@ export type IActivity = {
   snapshot?: ISnapshot;
   snapshot?: ISnapshot;
 };
 };
 
 
+export type ActivityWithPageTarget = IActivityHasId & {
+  target: IPageHasId;
+  user: IUserHasId;
+};
+
 export type IActivityHasId = IActivity & HasObjectId;
 export type IActivityHasId = IActivity & HasObjectId;
 
 
 export type ISearchFilter = {
 export type ISearchFilter = {

+ 14 - 1
apps/app/src/server/routes/apiv3/user-activities.ts

@@ -207,7 +207,20 @@ module.exports = (crowi: Crowi): Router => {
                 { $sort: { createdAt: -1 } },
                 { $sort: { createdAt: -1 } },
                 { $skip: offset },
                 { $skip: offset },
                 { $limit: limit },
                 { $limit: limit },
-
+                {
+                  $lookup: {
+                    from: 'pages',
+                    localField: 'target',
+                    foreignField: '_id',
+                    as: 'target',
+                  },
+                },
+                {
+                  $unwind: {
+                    path: '$target',
+                    preserveNullAndEmptyArrays: true,
+                  },
+                },
                 {
                 {
                   $lookup: {
                   $lookup: {
                     from: 'users',
                     from: 'users',

+ 0 - 2
apps/app/src/stores/recent-activity.ts

@@ -16,13 +16,11 @@ export const useSWRxRecentActivity = (
     endpoint,
     endpoint,
     limitParam,
     limitParam,
     offsetParam,
     offsetParam,
-    filterParam,
   ]) => {
   ]) => {
 
 
     const promise = apiv3Get<UserActivitiesResult>(endpoint, {
     const promise = apiv3Get<UserActivitiesResult>(endpoint, {
       limit: limitParam,
       limit: limitParam,
       offset: offsetParam,
       offset: offsetParam,
-      searchFilter: filterParam,
     });
     });
 
 
     return promise.then(result => result.data.serializedPaginationResult);
     return promise.then(result => result.data.serializedPaginationResult);