Browse Source

Merge pull request #10590 from growilabs/imprv/173985-display-name-and-link-to-page-in-activity-log

imprv: Show page name and link for affected pages in Activity Log
Yuki Takei 3 months ago
parent
commit
759ab4d488

+ 18 - 0
apps/app/src/client/components/ContentLinkButtons.tsx

@@ -38,6 +38,23 @@ const RecentlyCreatedLinkButton = React.memo(() => {
 
 RecentlyCreatedLinkButton.displayName = 'RecentlyCreatedLinkButton';
 
+const RecentActivityLinkButton = React.memo(() => {
+  const { t } = useTranslation();
+  return (
+    <ScrollLink to="recent-activity-list" offset={-120}>
+      <button
+        type="button"
+        className="btn btn-sm btn-outline-neutral-secondary rounded-pill d-flex align-items-center w-100 px-3"
+      >
+        <span className="material-symbols-outlined mx-1">update</span>
+        <span>{t('user_home_page.recent_activity')}</span>
+      </button>
+    </ScrollLink>
+  );
+});
+
+RecentActivityLinkButton.displayName = 'RecentActivityLinkButton';
+
 
 export type ContentLinkButtonsProps = {
   author?: IUserHasId,
@@ -54,6 +71,7 @@ export const ContentLinkButtons = (props: ContentLinkButtonsProps): JSX.Element
     <div className="d-grid gap-2">
       <BookMarkLinkButton />
       <RecentlyCreatedLinkButton />
+      <RecentActivityLinkButton />
     </div>
   );
 };

+ 82 - 13
apps/app/src/client/components/RecentActivity/ActivityListItem.tsx

@@ -1,9 +1,10 @@
 import { formatDistanceToNow } from 'date-fns';
-import { useTranslation } from 'next-i18next';
 import { type Locale } from 'date-fns/locale';
-import { getLocale } from '~/server/util/locale-utils';
-import type { ActivityHasUserId, SupportedActivityActionType } from '~/interfaces/activity';
+import { useTranslation } from 'next-i18next';
+
+import type { SupportedActivityActionType, ActivityHasTargetPage } from '~/interfaces/activity';
 import { ActivityLogActions } from '~/interfaces/activity';
+import { getLocale } from '~/server/util/locale-utils';
 
 
 export const ActivityActionTranslationMap: Record<
@@ -36,6 +37,18 @@ export const IconActivityTranslationMap: Record<
   [ActivityLogActions.ACTION_COMMENT_CREATE]: 'comment',
 };
 
+type ActivityListItemProps = {
+  activity: ActivityHasTargetPage,
+}
+
+type AllowPageDisplayPayload = {
+  grant: number | undefined,
+  status: string,
+  wip: boolean,
+  deletedAt?: Date,
+  path: string,
+}
+
 const translateAction = (action: SupportedActivityActionType): string => {
   return ActivityActionTranslationMap[action] || 'unknown_action';
 };
@@ -53,29 +66,85 @@ const calculateTimePassed = (date: Date, locale: Locale): string => {
   return timePassed;
 };
 
+const pageAllowedForDisplay = (allowDisplayPayload: AllowPageDisplayPayload): boolean => {
+  const {
+    grant, status, wip, deletedAt,
+  } = allowDisplayPayload;
+  if (grant !== 1) return false;
+
+  if (status !== 'published') return false;
+
+  if (wip) return false;
+
+  if (deletedAt) return false;
+
+  return true;
+};
+
+const setPath = (path: string, allowed: boolean): string => {
+  if (allowed) return path;
 
-export const ActivityListItem = ({ activity }: { activity: ActivityHasUserId }): JSX.Element => {
+  return '';
+};
+
+
+export const ActivityListItem = ({ props }: { props: ActivityListItemProps }): JSX.Element => {
   const { t, i18n } = useTranslation();
   const currentLangCode = i18n.language;
   const dateFnsLocale = getLocale(currentLangCode);
 
+  const { activity } = props;
+
+  const {
+    path, grant, status, wip, deletedAt,
+  } = activity.target;
+
+
+  const allowDisplayPayload: AllowPageDisplayPayload = {
+    grant,
+    status,
+    wip,
+    deletedAt,
+    path,
+  };
+
+  const isPageAllowed = pageAllowedForDisplay(allowDisplayPayload);
+
   const action = activity.action as SupportedActivityActionType;
   const keyToTranslate = translateAction(action);
   const fullKeyPath = `user_home_page.${keyToTranslate}`;
 
   return (
     <div className="activity-row">
-      <p className="mb-1">
-        <span className="material-symbols-outlined me-2">{setIcon(action)}</span>
-
-        <span className="dark:text-white">
-          {' '}{t(fullKeyPath)}
+      <div className="d-flex align-items-center">
+        <span className="material-symbols-outlined me-2 flex-shrink-0">
+          {setIcon(action)}
         </span>
 
-        <span className="text-secondary small ms-3">
-          {calculateTimePassed(activity.createdAt, dateFnsLocale)}
-        </span>
-      </p>
+        <div className="flex-grow-1 ms-2">
+          <div className="activity-path-line mb-0">
+            <a
+              href={setPath(path, isPageAllowed)}
+              className="activity-target-link fw-bold text-wrap d-block"
+            >
+              <span>
+                {setPath(path, isPageAllowed)}
+              </span>
+            </a>
+          </div>
+
+          <div className="activity-details-line d-flex">
+            <span>
+              {t(fullKeyPath)}
+            </span>
+
+            <span className="text-secondary small ms-3 align-self-center">
+              {calculateTimePassed(activity.createdAt, dateFnsLocale)}
+            </span>
+
+          </div>
+        </div>
+      </div>
     </div>
   );
 };

+ 8 - 6
apps/app/src/client/components/RecentActivity/RecentActivity.tsx

@@ -3,7 +3,7 @@ import React, {
 } from 'react';
 
 import { toastError } from '~/client/util/toastr';
-import type { IActivityHasId, ActivityHasUserId } from '~/interfaces/activity';
+import type { IActivityHasId, ActivityHasTargetPage } from '~/interfaces/activity';
 import { useSWRxRecentActivity } from '~/stores/recent-activity';
 import loggerFactory from '~/utils/logger';
 
@@ -18,15 +18,17 @@ type RecentActivityProps = {
   userId: string,
 }
 
-const hasUser = (activity: IActivityHasId): activity is ActivityHasUserId => {
+const hasTargetPage = (activity: IActivityHasId): activity is ActivityHasTargetPage => {
   return activity.user != null
-        && typeof activity.user === 'object';
+         && typeof activity.user === 'object'
+         && activity.target != null
+         && typeof activity.target === 'object';
 };
 
 export const RecentActivity = (props: RecentActivityProps): JSX.Element => {
   const { userId } = props;
 
-  const [activities, setActivities] = useState<ActivityHasUserId[]>([]);
+  const [activities, setActivities] = useState<ActivityHasTargetPage[]>([]);
   const [activePage, setActivePage] = useState(1);
   const [limit] = useState(10);
   const [offset, setOffset] = useState(0);
@@ -49,7 +51,7 @@ export const RecentActivity = (props: RecentActivityProps): JSX.Element => {
 
     if (paginatedData) {
       const activitiesWithPages = paginatedData.docs
-        .filter(hasUser);
+        .filter(hasTargetPage);
 
       setActivities(activitiesWithPages);
     }
@@ -63,7 +65,7 @@ export const RecentActivity = (props: RecentActivityProps): JSX.Element => {
       <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} />
+            <ActivityListItem props={{ activity }} />
           </li>
         ))}
       </ul>

+ 3 - 3
apps/app/src/client/components/UsersHomepageFooter.tsx

@@ -47,11 +47,11 @@ export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Elemen
           <RecentCreated userId={creatorId} />
         </div>
 
-        <h2 id="user-created-list" className="grw-user-page-header border-bottom pb-2 mb-3 d-flex">
-          <span className="growi-custom-icons me-1">recently_created</span>
+        <h2 id="recent-activity-list" className="grw-user-page-header border-bottom pb-2 mb-3 d-flex">
+          <span className="material-symbols-outlined me-1 fs-1">update</span>
           {t('user_home_page.recent_activity')}
         </h2>
-        <div id="user-created-list" className={`page-list ${styles['page-list']}`}>
+        <div id="recent-activity-list" className={`page-list ${styles['page-list']}`}>
           <RecentActivity userId={creatorId} />
         </div>
       </div>

+ 20 - 5
apps/app/src/interfaces/activity.ts

@@ -663,18 +663,33 @@ export type IActivity = {
   snapshot?: ISnapshot;
 };
 
+export type IActivityHasId = IActivity & HasObjectId;
+
 export type ActivityHasUserId = IActivityHasId & {
   user: IUserHasId;
 };
 
-export type IActivityHasId = IActivity & HasObjectId;
+export type ActivityHasTargetPage = IActivityHasId & {
+  user: IUserHasId;
+  target: IPopulatedPageTarget;
+};
+
+import type { PageGrant } from '@growi/core';
+export interface IPopulatedPageTarget {
+  _id: string;
+  path: string;
+  status: string;
+  grant?: PageGrant;
+  wip: boolean;
+  deletedAt: Date;
+}
+
+export interface PopulatedUserActivitiesResult {
+  serializedPaginationResult: PaginateResult<ActivityHasTargetPage>;
+}
 
 export type ISearchFilter = {
   usernames?: string[];
   dates?: { startDate: string | null; endDate: string | null };
   actions?: SupportedActionType[];
 };
-
-export interface UserActivitiesResult {
-  serializedPaginationResult: PaginateResult<IActivityHasId>;
-}

+ 4 - 4
apps/app/src/stores/recent-activity.ts

@@ -3,8 +3,8 @@ import useSWRImmutable from 'swr/immutable';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
 import type {
-  IActivityHasId,
-  UserActivitiesResult,
+  ActivityHasTargetPage,
+  PopulatedUserActivitiesResult,
 } from '~/interfaces/activity';
 import type { PaginateResult } from '~/interfaces/mongoose-utils';
 
@@ -12,14 +12,14 @@ export const useSWRxRecentActivity = (
   limit?: number,
   offset?: number,
   targetUserId?: string,
-): SWRResponse<PaginateResult<IActivityHasId>, Error> => {
+): SWRResponse<PaginateResult<ActivityHasTargetPage>, Error> => {
   const shouldFetch = targetUserId && targetUserId.length > 0;
   const key = shouldFetch
     ? ['/user-activities', limit, offset, targetUserId]
     : null;
 
   const fetcher = ([endpoint, limitParam, offsetParam, targetUserIdParam]) => {
-    const promise = apiv3Get<UserActivitiesResult>(endpoint, {
+    const promise = apiv3Get<PopulatedUserActivitiesResult>(endpoint, {
       limit: limitParam,
       offset: offsetParam,
       targetUserId: targetUserIdParam,