Skip to content

feat: Define actions that result in Create vs Edit activities#10818

Merged
mergify[bot] merged 31 commits intomasterfrom
feat/177877-activity-create-update-logic
Apr 15, 2026
Merged

feat: Define actions that result in Create vs Edit activities#10818
mergify[bot] merged 31 commits intomasterfrom
feat/177877-activity-create-update-logic

Conversation

@arvid-e
Copy link
Copy Markdown
Contributor

@arvid-e arvid-e commented Feb 26, 2026

@arvid-e arvid-e requested a review from miya March 16, 2026 07:07
@miya miya requested a review from Copilot March 18, 2026 05:47
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refines how page updates are recorded as activities by suppressing certain “edit/update” activity emissions based on recent activity history and revision count, aligning activity generation with the intended Create vs Edit behavior.

Changes:

  • Add logic in the APIV3 page update route to look up the most recent CREATE/UPDATE activity for the page.
  • Suppress emitting an UPDATE activity for rapid successive edits by the same user (within a 5-minute window), with additional gating based on revision count.
  • Keep existing notification flows (global notification, Slack, AI vector store rebuild) while conditionally emitting the activity update event.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +164 to +168
if (shouldGenerateUpdateActivity) {
// persist activity
const creator =
updatedPage.creator != null
? getIdForRef(updatedPage.creator)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACTION_UNSETTLED のアクティビティがそのまま残るのであれば問題なさそう?

Comment on lines +127 to +183
const lastContentActivity = await Activity.findOne({
target: targetPageId,
action: {
$in: [
SupportedAction.ACTION_PAGE_CREATE,
SupportedAction.ACTION_PAGE_UPDATE,
],
},
_id: { $ne: res.locals.activity?._id },
}).sort({ createdAt: -1 });

// Check if last updated user is me
const isLastActivityByMe =
lastContentActivity &&
lastContentActivity.user._id.toString() === req.user._id.toString();

// Get time since activity
const lastActivityTime = lastContentActivity
? lastContentActivity.createdAt.getTime()
: 0;
const timeSinceLastActivityMs = Date.now() - lastActivityTime;

// Decide if UPDATE activity should generate
let shouldGenerateUpdateActivity: boolean;
if (!isLastActivityByMe) {
shouldGenerateUpdateActivity = true;
} else if (timeSinceLastActivityMs < suppressUpdateWindow) {
shouldGenerateUpdateActivity = false;
} else {
const Revision = mongoose.model<IRevisionHasId>('Revision');
const revisionCount = await Revision.countDocuments({
pageId: updatedPage._id,
});

shouldGenerateUpdateActivity = revisionCount > minimumRevisionForActivity;
}

if (shouldGenerateUpdateActivity) {
// persist activity
const creator =
updatedPage.creator != null
? getIdForRef(updatedPage.creator)
: undefined;
const parameters = {
targetModel: SupportedTargetModel.MODEL_PAGE,
target: updatedPage,
action: SupportedAction.ACTION_PAGE_UPDATE,
};
const activityEvent = crowi.events.activity;
activityEvent.emit(
'update',
res.locals.activity._id,
parameters,
{ path: updatedPage.path, creator },
preNotifyService.generatePreNotify,
);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

対応した方がいいと思います

Comment on lines +140 to +141
lastContentActivity &&
lastContentActivity.user._id.toString() === req.user._id.toString();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user が存在しないケースは存在するのか?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

前編集したユーザーが削除された場合はlastContentActivity.user がnullになるかもしれないので対応しました

Comment on lines +121 to +122
const minimumRevisionForActivity = 2;
const suppressUpdateWindow = 5 * 60 * 1000; // 5 min
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これは定数として route の外に出しおきたいです

Comment on lines +127 to +183
const lastContentActivity = await Activity.findOne({
target: targetPageId,
action: {
$in: [
SupportedAction.ACTION_PAGE_CREATE,
SupportedAction.ACTION_PAGE_UPDATE,
],
},
_id: { $ne: res.locals.activity?._id },
}).sort({ createdAt: -1 });

// Check if last updated user is me
const isLastActivityByMe =
lastContentActivity &&
lastContentActivity.user._id.toString() === req.user._id.toString();

// Get time since activity
const lastActivityTime = lastContentActivity
? lastContentActivity.createdAt.getTime()
: 0;
const timeSinceLastActivityMs = Date.now() - lastActivityTime;

// Decide if UPDATE activity should generate
let shouldGenerateUpdateActivity: boolean;
if (!isLastActivityByMe) {
shouldGenerateUpdateActivity = true;
} else if (timeSinceLastActivityMs < suppressUpdateWindow) {
shouldGenerateUpdateActivity = false;
} else {
const Revision = mongoose.model<IRevisionHasId>('Revision');
const revisionCount = await Revision.countDocuments({
pageId: updatedPage._id,
});

shouldGenerateUpdateActivity = revisionCount > minimumRevisionForActivity;
}

if (shouldGenerateUpdateActivity) {
// persist activity
const creator =
updatedPage.creator != null
? getIdForRef(updatedPage.creator)
: undefined;
const parameters = {
targetModel: SupportedTargetModel.MODEL_PAGE,
target: updatedPage,
action: SupportedAction.ACTION_PAGE_UPDATE,
};
const activityEvent = crowi.events.activity;
activityEvent.emit(
'update',
res.locals.activity._id,
parameters,
{ path: updatedPage.path, creator },
preNotifyService.generatePreNotify,
);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

対応した方がいいと思います


// Check if last updated user is me
const isLastActivityByMe =
lastContentActivity &&
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null チェックをしましょう

Comment on lines +140 to +141
lastContentActivity &&
lastContentActivity.user._id.toString() === req.user._id.toString();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user が存在しないケースは存在するのか?

Comment on lines +164 to +168
if (shouldGenerateUpdateActivity) {
// persist activity
const creator =
updatedPage.creator != null
? getIdForRef(updatedPage.creator)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACTION_UNSETTLED のアクティビティがそのまま残るのであれば問題なさそう?

{ path: updatedPage.path, creator },
preNotifyService.generatePreNotify,
);
const minimumRevisionForActivity = 2;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これは 2 でいいんでしたっけ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2が正しいでした:

  • Revision 1: ページ作成
  • Revision 2: 初めての編集 (activityが発生しない)
  • Revision 3: 2つ目の編集 (activityが発生する)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

実装と仕様書 (dev wiki) を一致させてください

@arvid-e arvid-e requested a review from miya March 24, 2026 06:14
Comment on lines +55 to +56
const minimumRevisionForActivity = 2;
const suppressUpdateWindowMs = 5 * 60 * 1000; // 5 min
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

定数は大文字且つ snake case にしたいです

lastContentActivity?.user?._id?.toString() ===
req.user?._id?.toString();

const lastActivityTime = lastContentActivity
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null チェックをしてください

{ path: updatedPage.path, creator },
preNotifyService.generatePreNotify,
);
const minimumRevisionForActivity = 2;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

実装と仕様書 (dev wiki) を一致させてください

@miya miya requested a review from yuki-takei March 24, 2026 07:32
Comment thread apps/app/src/server/routes/apiv3/page/update-page.ts
@arvid-e arvid-e requested a review from yuki-takei March 31, 2026 07:34
Comment thread apps/app/src/server/service/activity/update-activity-logic.ts
@arvid-e arvid-e requested a review from yuki-takei April 8, 2026 08:16
try {
const targetPageId = updatedPage._id.toString();
const currentActivityId = res.locals.activity?._id.toString();
const currentUserId = req.user?._id?.toString();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentUserId が undefined の場合、isLastActivityByMe は常に false となり、常にアクティビティが生成されてしまう。認証済みルートなので実際には起きにくいが、型定義を string | undefined に明示し、undefined時の振る舞いを意図的にすべき。


const isLastActivityByMe =
!!currentUserId &&
lastContentActivity?.user?._id?.toString() === currentUserId;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { getIdForRef, getIdStringForRef } from '@growi/core';

を利用してください

@arvid-e arvid-e requested a review from yuki-takei April 10, 2026 06:30
Comment thread apps/app/src/server/service/activity/update-activity.spec.ts
Comment thread apps/app/src/server/routes/apiv3/page/update-page.ts
@arvid-e arvid-e requested a review from yuki-takei April 14, 2026 07:51
@mergify mergify Bot added the queued label Apr 15, 2026
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented Apr 15, 2026

Merge Queue Status

This pull request spent 11 minutes 27 seconds in the queue, including 10 minutes 57 seconds running CI.

Required conditions to merge
  • -check-failure ~= ci-app-
  • -check-failure ~= ci-slackbot-
  • -check-failure ~= test-prod-node24 /
  • check-success = test-prod-node24 / build-prod
  • check-success ~= ci-app-launch-dev
  • check-success ~= ci-app-lint
  • check-success ~= ci-app-test
  • check-success ~= test-prod-node24 / launch-prod
  • check-success ~= test-prod-node24 / run-playwright

mergify Bot added a commit that referenced this pull request Apr 15, 2026
@mergify mergify Bot merged commit ced383b into master Apr 15, 2026
24 checks passed
@mergify mergify Bot deleted the feat/177877-activity-create-update-logic branch April 15, 2026 10:26
@mergify mergify Bot removed the queued label Apr 15, 2026
This was referenced Apr 15, 2026
@github-actions github-actions Bot mentioned this pull request Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants