Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
91516ab
Add user actions for the activity log
arvid-e Oct 2, 2025
73a3991
Copy user activity api from last branch
arvid-e Oct 2, 2025
b97512d
Reinstall using node 20
arvid-e Oct 2, 2025
1c85929
Merge branch 'feat/171499-activity-log' into feat/171501-api-for-acti…
yuki-takei Oct 3, 2025
39db017
Revert "Reinstall using node 20"
yuki-takei Oct 3, 2025
0e4d3f9
Merge branch 'feat/171499-activity-log' into feat/171501-api-for-acti…
yuki-takei Oct 3, 2025
84946c3
fix biome error
yuki-takei Oct 3, 2025
9c68ca8
Update error code
arvid-e Oct 6, 2025
e30c374
Use audit log hook as template and define search filter
arvid-e Oct 7, 2025
3362901
Remove the need of type casting using request interfaces
arvid-e Oct 8, 2025
a2aa6c1
Make use of generic custom request to guarantee limit and offset types
arvid-e Oct 14, 2025
3390062
Merge pull request #10368 from growilabs/feat/171501-api-for-activity…
arvid-e Oct 14, 2025
808b8ca
Merge branch 'feat/171499-activity-log' of into feat/172382-data-hoo…
arvid-e Oct 14, 2025
7f28e29
Clean up comments
arvid-e Oct 14, 2025
8c82dd8
Fix one biome error due to line length
arvid-e Oct 15, 2025
15524d7
Add interfaces for api return values
arvid-e Oct 15, 2025
72a290b
Define return type for the apiv3 get call and fix mistake in interface
arvid-e Oct 15, 2025
105c19d
Remove search filter as the server side is already getting userId and…
arvid-e Oct 15, 2025
8583791
Remove not needed interface for supported actions
arvid-e Oct 15, 2025
659b54a
Merge pull request #10397 from growilabs/feat/172382-data-hook-recent…
yuki-takei Oct 15, 2025
50c3f90
Create recent activity react component and copy over reusable elements
arvid-e Oct 15, 2025
eb2f26e
Fetch and render data in RecentActivity component
arvid-e Oct 20, 2025
b92f962
Remove not needed filter params from recent activity hook
arvid-e Oct 22, 2025
db561bb
Populate target from hook and implement a new list item for activities
arvid-e Oct 22, 2025
0f0ca46
Move ActivityWithPageTarget to interface file
arvid-e Oct 22, 2025
db1b188
Translate action names into readable text
arvid-e Oct 22, 2025
3ffb18f
Fix biome error
arvid-e Oct 22, 2025
9a7310e
Remove not needed populate field since aggregation pipeline is used
arvid-e Oct 27, 2025
61ab119
Move action translation map to view layer
arvid-e Oct 27, 2025
1b773bf
Merge pull request #10436 from growilabs/feat/172383-recent-activity-…
yuki-takei Oct 27, 2025
781879d
Update displayable actions
arvid-e Oct 29, 2025
5e1d584
Display only the action and date of the activity in the log
arvid-e Oct 29, 2025
8a993d2
Make userId come from parameter instead of logged in user
arvid-e Oct 29, 2025
754759c
Make use of the user page userId
arvid-e Oct 29, 2025
87aba11
Try running the test pipeline again
arvid-e Oct 29, 2025
5988189
Merge pull request #10448 from growilabs/feat/173165-only-display-action
yuki-takei Oct 29, 2025
8a3f366
Merge branch 'feat/171499-activity-log' into fix/173307-can-only-see-…
arvid-e Oct 29, 2025
18d1997
Add userTargetId to validator and request
arvid-e Nov 4, 2025
d4e4844
Use current user id when target id is not provided
arvid-e Nov 4, 2025
35e1b3d
Improve readability of the log entries
arvid-e Nov 4, 2025
7c61cd8
Run test pipeline again
arvid-e Nov 4, 2025
e2d5c70
Merge pull request #10449 from growilabs/fix/173307-can-only-see-logg…
yuki-takei Nov 5, 2025
6679b91
Merge pull request #10464 from growilabs/imprv/173008-improve-activit…
yuki-takei Nov 5, 2025
84536bc
Add title for activity log in different languages
arvid-e Nov 5, 2025
7bbbdcb
Add translations for log entries
arvid-e Nov 7, 2025
5dcf5c3
Handle unknown actions
arvid-e Nov 7, 2025
892382d
Add new icons
arvid-e Nov 7, 2025
c9b1f74
Correctly handle unknown actions
arvid-e Nov 7, 2025
39ce997
Merge pull request #10485 from growilabs/imprv/173842-improve-design-…
yuki-takei Nov 7, 2025
c1c56fb
Change unknown action translations
arvid-e Nov 10, 2025
1e821d9
Merge branch 'feat/171499-activity-log' into feat/173660-add-title-to…
arvid-e Nov 10, 2025
d3bb766
Merge pull request #10471 from growilabs/feat/173660-add-title-to-act…
arvid-e Nov 10, 2025
a487703
Fix merge conflict mistake
arvid-e Nov 10, 2025
f5cccfe
Only load pagination if needed
arvid-e Nov 10, 2025
d553200
Revert "Only load pagination if needed"
arvid-e Nov 10, 2025
5fab149
Hide pagination when not needed
arvid-e Nov 10, 2025
5fa1ec4
add --diagnostic-level option
arvid-e Nov 10, 2025
cf8f9a9
Merge remote-tracking branch 'origin/master' into feat/171499-activit…
arvid-e Nov 10, 2025
538f75e
Merge branch 'feat/171499-activity-log' into feat/173905-hide-paginat…
arvid-e Nov 10, 2025
3793c7e
fix biome errors
arvid-e Nov 10, 2025
e62b552
Merge branch 'feat/171499-activity-log' into feat/173905-hide-paginat…
arvid-e Nov 10, 2025
2675595
Translate timestamps
arvid-e Nov 10, 2025
7c85b4b
Fix lint comma error
arvid-e Nov 10, 2025
c0744ab
Merge pull request #10488 from growilabs/feat/173905-hide-pagination-…
yuki-takei Nov 10, 2025
08d5d57
Export language locale method to util
arvid-e Nov 10, 2025
19a329f
Move util to existing file
arvid-e Nov 10, 2025
b973235
Fix indendation on language map
arvid-e Nov 10, 2025
31749f8
Format util file
arvid-e Nov 10, 2025
0e978d9
Revert format
arvid-e Nov 10, 2025
039eecd
Handle biome errors
arvid-e Nov 10, 2025
f22f185
Fix import sorting
arvid-e Nov 10, 2025
6c2ff30
Add spaces after imports
arvid-e Nov 10, 2025
acdc6bb
Change import order
arvid-e Nov 10, 2025
6b1ce07
Remove spaces
arvid-e Nov 10, 2025
aef9f96
Merge pull request #10490 from growilabs/imprv/173926-add-timestamp-t…
yuki-takei Nov 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"launch-dev:ci": "cross-env NODE_ENV=development pnpm run dev:migrate && pnpm run ts-node src/server/app.ts --ci",
"lint:typecheck": "vue-tsc --noEmit",
"lint:eslint": "eslint --quiet \"**/*.{js,mjs,jsx,ts,mts,tsx}\"",
"lint:biome": "biome check",
"lint:biome": "biome check --diagnostic-level=error",
"lint:styles": "stylelint \"src/**/*.scss\"",
"lint:openapi:apiv3": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv3.json",
"lint:openapi:apiv1": "node node_modules/swagger2openapi/oas-validate tmp/openapi-spec-apiv1.json",
Expand Down
13 changes: 12 additions & 1 deletion apps/app/public/static/locales/en_US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,18 @@
},
"user_home_page": {
"bookmarks": "Bookmarks",
"recently_created": "Recently Created"
"recently_created": "Recently Created",
"recent_activity": "Recent Activity",
"unknown_action": "made an unspecified change",
"page_create": "created a page",
"page_update": "updated a page",
"page_delete": "deleted a page",
"page_delete_completely": "deleted a page",
"page_rename": "renamed a page",
"page_revert": "reverted a page",
"page_like": "liked a page",
"page_duplicate": "duplicated a page",
"comment_create": "posted a comment"
},
"bookmark_folder": {
"bookmark_folder": "bookmark folder",
Expand Down
13 changes: 12 additions & 1 deletion apps/app/public/static/locales/fr_FR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -993,7 +993,18 @@
},
"user_home_page": {
"bookmarks": "Favoris",
"recently_created": "Page récentes"
"recently_created": "Page récentes",
"recent_activity": "Activité récente",
"unknown_action": "a effectué une modification non spécifiée",
"page_create": "a créé une page",
"page_update": "a mis à jour une page",
"page_delete": "a supprimé une page",
"page_delete_completely": "a supprimé complètement une page",
"page_rename": "a renommé une page",
"page_revert": "a restauré une page",
"page_duplicate": "a dupliqué une page",
"page_like": "a aimé une page",
"comment_create": "a publié un commentaire"
},
"bookmark_folder": {
"bookmark_folder": "dossier de favoris",
Expand Down
13 changes: 12 additions & 1 deletion apps/app/public/static/locales/ja_JP/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,18 @@
},
"user_home_page": {
"bookmarks": "ブックマーク",
"recently_created": "最近作成したページ"
"recently_created": "最近作成したページ",
"recent_activity": "最近のアクティビティ",
"unknown_action": "未指定の変更を加えました",
"page_create": "ページを作成しました",
"page_update": "ページを更新しました",
"page_delete": "ページを削除しました",
"page_delete_completely": "ページを完全に削除しました",
"page_rename": "ページの名前を変更しました",
"page_revert": "ページを元に戻しました",
"page_duplicate": "ページを複製しました",
"page_like": "ページをいいねしました",
"comment_create": "コメントを投稿しました"
},
"bookmark_folder": {
"bookmark_folder": "ブックマークフォルダ",
Expand Down
13 changes: 12 additions & 1 deletion apps/app/public/static/locales/ko_KR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -959,7 +959,18 @@
},
"user_home_page": {
"bookmarks": "북마크",
"recently_created": "최근 생성됨"
"recently_created": "최근 생성됨",
"recent_activity": "최근 활동",
"unknown_action": "지정되지 않은 변경 사항을 적용했습니다",
"page_create": "페이지를 생성했습니다",
"page_update": "페이지를 업데이트했습니다",
"page_delete": "페이지를 삭제했습니다",
"page_delete_completely": "페이지를 완전히 삭제했습니다",
"page_rename": "페이지 이름을 변경했습니다",
"page_revert": "페이지를 되돌렸습니다",
"page_duplicate": "페이지를 복제했습니다",
"page_like": "페이지에 좋아요를 눌렀습니다",
"comment_create": "댓글을 게시했습니다"
},
"bookmark_folder": {
"bookmark_folder": "북마크 폴더",
Expand Down
13 changes: 12 additions & 1 deletion apps/app/public/static/locales/zh_CN/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -1004,7 +1004,18 @@
},
"user_home_page": {
"bookmarks": "书签",
"recently_created": "最近创建页面"
"recently_created": "最近创建页面",
"recent_activity": "最近动态",
"unknown_action": "进行了未指明的更改",
"page_create": "创建了页面",
"page_update": "更新了页面",
"page_delete": "删除了页面",
"page_delete_completely": "彻底删除了页面",
"page_rename": "重命名了页面",
"page_revert": "还原了页面",
"page_duplicate": "复制了页面",
"page_like": "赞了页面",
"comment_create": "发布了评论"
},
"bookmark_folder": {
"bookmark_folder": "书签文件夹",
Expand Down
81 changes: 81 additions & 0 deletions apps/app/src/client/components/RecentActivity/ActivityListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
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 { ActivityLogActions } from '~/interfaces/activity';


export const ActivityActionTranslationMap: Record<
SupportedActivityActionType,
string
> = {
[ActivityLogActions.ACTION_PAGE_CREATE]: 'page_create',
[ActivityLogActions.ACTION_PAGE_UPDATE]: 'page_update',
[ActivityLogActions.ACTION_PAGE_DELETE]: 'page_delete',
[ActivityLogActions.ACTION_PAGE_DELETE_COMPLETELY]: 'page_delete_completely',
[ActivityLogActions.ACTION_PAGE_RENAME]: 'page_rename',
[ActivityLogActions.ACTION_PAGE_REVERT]: 'page_revert',
[ActivityLogActions.ACTION_PAGE_DUPLICATE]: 'page_duplicate',
[ActivityLogActions.ACTION_PAGE_LIKE]: 'page_like',
[ActivityLogActions.ACTION_COMMENT_CREATE]: 'comment_create',
};

export const IconActivityTranslationMap: Record<
SupportedActivityActionType,
string
> = {
[ActivityLogActions.ACTION_PAGE_CREATE]: 'add_box',
[ActivityLogActions.ACTION_PAGE_UPDATE]: 'edit',
[ActivityLogActions.ACTION_PAGE_DELETE]: 'delete',
[ActivityLogActions.ACTION_PAGE_DELETE_COMPLETELY]: 'delete_forever',
[ActivityLogActions.ACTION_PAGE_RENAME]: 'label',
[ActivityLogActions.ACTION_PAGE_REVERT]: 'undo',
[ActivityLogActions.ACTION_PAGE_DUPLICATE]: 'content_copy',
[ActivityLogActions.ACTION_PAGE_LIKE]: 'favorite',
[ActivityLogActions.ACTION_COMMENT_CREATE]: 'comment',
};

const translateAction = (action: SupportedActivityActionType): string => {
return ActivityActionTranslationMap[action] || 'unknown_action';
};

const setIcon = (action: SupportedActivityActionType): string => {
return IconActivityTranslationMap[action] || 'question_mark';
};

const calculateTimePassed = (date: Date, locale: Locale): string => {
const timePassed = formatDistanceToNow(date, {
addSuffix: true,
locale,
});

return timePassed;
};


export const ActivityListItem = ({ activity }: { activity: ActivityHasUserId }): JSX.Element => {
const { t, i18n } = useTranslation();
const currentLangCode = i18n.language;
const dateFnsLocale = getLocale(currentLangCode);

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)}
</span>

<span className="text-secondary small ms-3">
{calculateTimePassed(activity.createdAt, dateFnsLocale)}
</span>
</p>
</div>
);
};
83 changes: 83 additions & 0 deletions apps/app/src/client/components/RecentActivity/RecentActivity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, {
useState, useCallback, useEffect, type JSX,
} from 'react';

import { toastError } from '~/client/util/toastr';
import type { IActivityHasId, ActivityHasUserId } 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');

type RecentActivityProps = {
userId: string,
}

const hasUser = (activity: IActivityHasId): activity is ActivityHasUserId => {
return activity.user != null
&& typeof activity.user === 'object';
};

export const RecentActivity = (props: RecentActivityProps): JSX.Element => {
const { userId } = props;

const [activities, setActivities] = useState<ActivityHasUserId[]>([]);
const [activePage, setActivePage] = useState(1);
const [limit] = useState(10);
const [offset, setOffset] = useState(0);

const { data: paginatedData, error } = useSWRxRecentActivity(limit, offset, userId);

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(hasUser);

setActivities(activitiesWithPages);
}
}, [paginatedData, error]);

const totalItemsCount = paginatedData?.totalDocs || 0;
const needsPagination = totalItemsCount > limit;

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>

{needsPagination && (
<PaginationWrapper
activePage={activePage}
changePage={handlePage}
totalItemsCount={totalItemsCount}
pagingLimit={limit}
align="center"
size="sm"
/>
)}
</div>
);
};
9 changes: 9 additions & 0 deletions apps/app/src/client/components/UsersHomepageFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState, type JSX } from 'react';

import { useTranslation } from 'next-i18next';

import { RecentActivity } from '~/client/components/RecentActivity/RecentActivity';
import { RecentCreated } from '~/client/components/RecentCreated/RecentCreated';
import { useCurrentUser } from '~/stores-universal/context';

Expand Down Expand Up @@ -45,6 +46,14 @@ export const UsersHomepageFooter = (props: UsersHomepageFooterProps): JSX.Elemen
<div id="user-created-list" className={`page-list ${styles['page-list']}`}>
<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>
{t('user_home_page.recent_activity')}
</h2>
<div id="user-created-list" className={`page-list ${styles['page-list']}`}>
<RecentActivity userId={creatorId} />
</div>
</div>
</div>
);
Expand Down
34 changes: 32 additions & 2 deletions apps/app/src/interfaces/activity.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import type { HasObjectId, IUser, Ref } from '@growi/core';
import type {
HasObjectId,
IPageHasId,
IUser,
IUserHasId,
Ref,
} from '@growi/core';

import type { PaginateResult } from './mongoose-utils';

// Model
const MODEL_PAGE = 'Page';
Expand Down Expand Up @@ -377,6 +385,7 @@ export const SupportedAction = {

// Action required for notification
export const EssentialActionGroup = {
ACTION_PAGE_CREATE,
ACTION_PAGE_LIKE,
ACTION_PAGE_BOOKMARK,
ACTION_PAGE_UPDATE,
Expand Down Expand Up @@ -568,6 +577,18 @@ export const LargeActionGroup = {
ACTION_ADMIN_SEARCH_INDICES_REBUILD,
} as const;

export const ActivityLogActions = {
ACTION_PAGE_CREATE,
ACTION_PAGE_UPDATE,
ACTION_PAGE_RENAME,
ACTION_PAGE_DUPLICATE,
ACTION_PAGE_DELETE,
ACTION_PAGE_DELETE_COMPLETELY,
ACTION_PAGE_REVERT,
ACTION_PAGE_LIKE,
ACTION_COMMENT_CREATE,
} as const;

/*
* Array
*/
Expand Down Expand Up @@ -645,7 +666,8 @@ export type SupportedActionType =
(typeof SupportedAction)[keyof typeof SupportedAction];
export type SupportedActionCategoryType =
(typeof SupportedActionCategory)[keyof typeof SupportedActionCategory];

export type SupportedActivityActionType =
(typeof ActivityLogActions)[keyof typeof ActivityLogActions];
export type ISnapshot = Partial<Pick<IUser, 'username'>>;

export type IActivity = {
Expand All @@ -661,10 +683,18 @@ export type IActivity = {
snapshot?: ISnapshot;
};

export type ActivityHasUserId = IActivityHasId & {
user: IUserHasId;
};

export type IActivityHasId = IActivity & HasObjectId;

export type ISearchFilter = {
usernames?: string[];
dates?: { startDate: string | null; endDate: string | null };
actions?: SupportedActionType[];
};

export interface UserActivitiesResult {
serializedPaginationResult: PaginateResult<IActivityHasId>;
}
1 change: 1 addition & 0 deletions apps/app/src/server/routes/apiv3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ module.exports = (crowi, app) => {
router.use('/in-app-notification', require('./in-app-notification')(crowi));

router.use('/personal-setting', require('./personal-setting')(crowi));
router.use('/user-activities', require('./user-activities')(crowi));

router.use('/user-group-relations', require('./user-group-relation')(crowi));
router.use('/external-user-group-relations', require('~/features/external-user-group/server/routes/apiv3/external-user-group-relation')(crowi));
Expand Down
Loading
Loading