Skip to content

feat(plugin-detail): the record activity timeline folds consecutive updates by one actor into one row with a count - #26055

Merged
objectstack-fleet[bot] merged 18 commits into
mainfrom
claude/project-thread-nv45n5
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 18 commits into
mainfrom
claude/project-thread-nv45n5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #25959
Clause-②: yes
Metadata-protocol: none

What

The record activity timeline folds a run of consecutive update entries by one actor into one row with a count. This is the renderer half of #25787 (its server half, PR #25958, made each generic update summary name its changed fields).

  • Mapper (ui/packages/plugin-detail/src/renderers/recordActivityFeed.ts). activityRowToFeedItem now carries what it used to drop: the row's own sys_activity.type (activityType), its metadata.kind (activityKind), actor_id (actorId, null when the row names no user) and changed_fields (changedFields, only when the column is an array, so an unstamped null row is not read as "no fields"). Only metadata.kind is read from the metadata JSON; the recorded old/new values are never looked at.
  • Fold (foldFeedItems, same module, pure). A run is adjacent items of the list the reader sees (after the filter): each an updated row with no domain kind, by the same actorId AND the same display name (so two unnamed writers are never merged), each at most FOLD_WINDOW_MS = 10 minutes after its neighbour. Any other row ends the run. A run of one is an ordinary row.
  • Render (RecordActivityTimeline.tsx). The folded row shows the actor, the latest entry's time, N updates, and Updated FIELDS: the union of the changed fields, by the object's field label (from RecordContextProvider's objectSchema, through the same translated fieldLabel the record sections use) and by machine name outside a record page; three names, then N more. A button with aria-expanded expands the run to its entries, each rendered exactly as an unfolded row. The panel heading still counts every entry.
  • Types (@object-ui/types). Four new optional FeedItem members (Clause-② yes). A host item without them renders as before and never folds.
  • Strings. detail.foldedUpdates, detail.foldedUpdateCount (count family), detail.foldedMoreFields (count family), detail.expandFolded, detail.collapseFolded, appended after detail.viewSource in all ten packs and mirrored in DETAIL_DEFAULT_TRANSLATIONS. The field list is joined with the existing validation.formInvalidJoiner key, not Intl.ListFormat (outside the package's TS lib, and measured and rejected for field lists in form-invalid-toast-joiner.test.tsx).
  • scripts/sdui-manifest.record.json re-recorded; root changeset .changeset/25959-activity-fold.md (patch, @object-ui/types, @object-ui/plugin-detail, @object-ui/i18n).

No host plumbing: RecordDetailView already renders the discussion inside RecordContextProvider with objectSchema={objectDef}, so RecordDetailView.tsx is untouched.

Choices for the seat

  • Window: 10 minutes between neighbours, not from the first entry, so one flow writing every few minutes stays one run.
  • Adjacency after the filter. With "Field changes" selected, two updates separated by a (hidden) comment are adjacent on screen and fold. In "All activity" the comment ends the run.
  • Never fold: everything whose activityType is not updated (created, deleted, assigned, shared, completed, comments, host items) and any row with a metadata.kind (a fired milestone).

Tests

Head 1c8752a0 (after merging origin/main):

  • RecordActivityTimeline.fold-25959.test.tsx (new, 17 tests): the mapper carries the four members; foldFeedItems folds a run in order, splits on actor / system-vs-person / differing unnamed writers / a gap over the window, never folds the excluded kinds, counts entries without a summary; the timeline renders the thirteen-update campaign as one row with 13 updates and the field labels, keeps the comment visible, toggles aria-expanded and shows all 13 entries, falls back to machine names, truncates long lists, and reads a no-field run as Updated record.
  • Updated recordActivityFeed.test.ts (two exact item shapes gain activityType / actorId) and recordActivityFeed.milestoneKind-12106.test.tsx (the marked item also carries activityKind).
  • Reverse check: with the fold's updated test mutated to never match, the new file goes 9 failed / 8 passed of 17; restored by git checkout HEAD --, hash-verified.
  • Final run on 1c8752a0, under os-verify-lock.sh: vitest run packages/plugin-detail/ packages/i18n/ plus the app-shell activity and defaults-mirror tests (RecordDetailView.activity*, .feed*, .replyCountOnRead, .recordOpenWaves, .authoredActivityThreading, .discussionFallbackPipeline, defaults-maps-mirror-en-pack, activityItemType): Test Files 403 passed, 1 skipped (404); Tests 4327 passed, 31 skipped. The rest of app-shell is left to CI Test Core.
  • pnpm --filter @object-ui/types --filter @object-ui/plugin-detail --filter @object-ui/i18n run type-check: exit 0. eslint on the touched files: 0 errors.
  • Gates: dispatch-gates --commands derived 46 commands on 1c8752a0; all 46 exit 0, and --ran reconciles 46 derived / 46 run / 0 NOT-MEASURED (a derived zero). check-sdui-manifest included.

Acceptance notes

  • A milestone row that the read redaction served without its kind (the reader is not served a field the milestone watches) and whose declaration names no type arrives as a plain updated row and can fold. That is the redaction's documented behaviour, not a new gap.
  • Folding happens after applyFeedConfig's page window, so a page of limit entries can render as fewer rows. The window still counts entries, which is what "Load more" pages over.
  • The fold's expand state is keyed on the run's first entry id; "Load more" bringing an older entry into the same run collapses it again.

Generated by Claude Code

…e record activity timeline

The sys_activity -> FeedItem mapper now carries the raw activity type, the
row's metadata.kind, actor_id and changed_fields as optional FeedItem
members. RecordActivityTimeline folds runs of adjacent 'updated' rows by
one actor (at most 10 minutes apart) into one row with the entry count and
the union of the changed fields' labels; the row expands to its entries.

Claude-Session: https://claude.ai/code/session_01PoTpJPgXim1ybDgRyat4BY
Co-authored-by: Claude <noreply@anthropic.com>
…module

Keeps RecordActivityTimeline.tsx a components-only module (react-refresh).

Claude-Session: https://claude.ai/code/session_01PoTpJPgXim1ybDgRyat4BY
Co-authored-by: Claude <noreply@anthropic.com>
…list punctuation

Intl.ListFormat is outside the package's TS lib, and the repo measured and
rejected it for field lists (no separator in zh/ru unit styles).

Claude-Session: https://claude.ai/code/session_01PoTpJPgXim1ybDgRyat4BY
Co-authored-by: Claude <noreply@anthropic.com>
…-nv45n5

# Conflicts:
#	scripts/sdui-manifest.record.json
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 26, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 1c8752a096577b60d6c99b2bd797dcc31205596c
Local-runs: none

Scope: the declaration face Clause-②: yes (claim 6411723287). The committed tree touches no packages/spec/src/** and no governed path. Read: ui/packages/types/src/views.ts, ui/packages/plugin-detail/src/renderers/recordActivityFeed.ts, ui/packages/plugin-detail/src/index.tsx (entry exports at this head), and the changeset.

① Derived judgments

  • @object-ui/types FeedItem gains four optional members: activityType?: string, activityKind?: string, actorId?: string | number | null and changedFields?: string[]. The change is additive and optional, so every existing producer and consumer still type-checks, and a host item without them renders as before and never folds. This widening is the declared one, and it is correct.
  • @object-ui/plugin-detail activityRowToFeedItem (a root export) now returns those members. They are read only from type, actor_id, changed_fields and metadata.kind, and the recorded old/new values are never read. changedFields is set only when the served column is an array, so a null column is not read as "no fields". This is correct and exposes nothing the server did not already serve to this reader.
  • foldFeedItems / FOLD_WINDOW_MS / TimelineEntry are exported from the module but not from the package entry (index.tsx re-exports only the mapper, the type maps, the warning reset and isRefusedFeedRead), so they add no public surface. Correct.

② Semver level

Metadata-protocol: none, so patch applies to each moved package. The changeset declares patch for @object-ui/types, @object-ui/plugin-detail and @object-ui/i18n, which are exactly the packages the diff moves. Consistent.

③ Boundary flags

  • No permission boundary moves: changed_fields is the server's already-narrowed column, and the fold is rendering only.
  • The dev's open question is answered on the PR: keep the 10-minute neighbour window and judge adjacency after the filter.

Implemented-by: claude/project-thread-nv45n5
Reviewed-by: session_01SMps7wq7rBTk53e8KTSEup

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:ui seat 2 · session_01SMps7wq7rBTk53e8KTSEup · 2026-10-26T10:25Z · head 1c8752a0

  1. 形态: draft → main, assignee os-steve, mergeable. The first content line is Fixes #25959. That is right: this card is the renderer half, and record discussion feed: thirteen consecutive system entries "Updated Campaign \"CPG-0001 - …\"" with no field diff — an update event should carry what changed and consecutive same-actor updates should fold #25787's server half already landed. No other closing keyword appears. Clause-②: yes / Metadata-protocol: none lead their lines in both the body and the changeset.
  2. 前提实测: the seat read the fold at this head. isFoldableUpdate = activityType === 'updated', feed type field_change, no activityKind, not a reply. continuesRun = same actorId (null-normalised) AND same display name AND neighbours ≤ FOLD_WINDOW_MS (10 min), with an unparseable time ending the run. Any non-foldable row flushes the run, and a run of one is an ordinary row. The panel heading still counts every entry, and no data is merged.
  3. Contract review: declaration face, TIER_DEFAULT, PASS at this head (comment above).
  4. Seat answers to the open question: keep the 10-minute window between neighbours, and judge adjacency on the list the reader sees after the filter. A reader who filters to updates should see the run they are looking at folded.
  5. 接受的偏差: none. The 21 files sit inside the claimed surface. RecordDetailView.tsx is untouched, because it already provides objectSchema through RecordContextProvider. The list separator reuses validation.formInvalidJoiner, which is an existing key. The new detail.* keys are append-only in all ten packs.
  6. 门禁 / 落地: waits on Lint & Repo Gates and TypeScript Type Check success and all checks green at this head, then relay pr_ready + automerge_enable. No governed path. +854/−155, which is under the 3000-line threshold.

TimelineEntry is already exported by app-shell (buildDebugApi.ts); the
one-authority-per-exported-name pin refuses a second declaration.

Claude-Session: https://claude.ai/code/session_01PoTpJPgXim1ybDgRyat4BY
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat 2 · head update · 2026-10-26T10:52Z

FAIL round 1 (Test UI 14/16, one-authority-per-exported-name-6273: the new TimelineEntry collided with app-shell buildDebugApi.ts:40) was fixed at head ed63e3c5. The change is a rename only, to ActivityTimelineEntry, plus a re-recorded scripts/sdui-manifest.record.json. No KNOWN_COLLISIONS line was added. The file surface is unchanged, so ACCEPT 6413742583 carries over to ed63e3c5.

…-nv45n5

# Conflicts:
#	scripts/sdui-manifest.record.json
…ds after merging main

Claude-Session: https://claude.ai/code/session_01PoTpJPgXim1ybDgRyat4BY
Co-authored-by: Claude <noreply@anthropic.com>
…-nv45n5

# Conflicts:
#	scripts/sdui-manifest.sources/i18n.json
#	scripts/sdui-manifest.sources/types.json
…-nv45n5

# Conflicts:
#	scripts/sdui-manifest.sources/i18n.json
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么:记录的讨论/动态面板里,同一人(或系统)连续修改同一条记录、相邻间隔不超过 10 分钟的「更新」合成一行,显示次数和改过的字段名,点开可看每一条。
为什么改:HotCRM 演示活动 CPG-0001 讨论区连续 13 条「系统 · 更新了活动…」把人的评论淹没(#25959,#25787 的前端半)。
风险与回滚:只改显示,数据不删不合并;创建、删除、评论、里程碑永不折叠;FeedItem 多 4 个可选字段(Clause-② yes)。回滚 = revert 本 PR。
先看:ui/packages/plugin-detail/src/renderers/recordActivityFeed.ts(foldFeedItems、mapper 新字段)→ ui/packages/plugin-detail/src/RecordActivityTimeline.tsx(renderFold)→ ui/packages/types/src/views.ts(FeedItem)。
两分钟验证:cd ui && pnpm exec vitest run packages/plugin-detail/src/__tests__/RecordActivityTimeline.fold-25959,预期 17 个测试全过。
需要你做的:无,席位落地。
席位意见:ACCEPT。只改动态面板的显示,四轮补丁都只为跟上 main 的 sdui 记录;CI 全绿。

席位核对

seat 2 · 2026-10-26T23:05Z · head e7a0e424 (#25959)

item reading
form 21 files, +856/−157: plugin-detail recordActivityFeed.ts (foldFeedItems, mapper fields), RecordActivityTimeline.tsx (renderFold), useDetailTranslation.ts; types/src/views.ts FeedItem (4 optional fields, Clause-② yes); fold strings in the i18n locale packs; new 17-case pin RecordActivityTimeline.fold-25959, two existing pins extended; sources/i18n.json, plugin-detail.json, types.json; root .changeset/
premise measured earlier seat-2 ACCEPT stands on the behaviour; since then four patch rounds only merged main (merge commits) and re-recorded. Round on 71580254: re-record moved only sources/i18n.json (930 sources), check-sdui-manifest exit 0
gates (check-runs on e7a0e424) 61 runs: 54 success, 7 skipped (opt-in / filtered), 0 failed; no open review threads. The dev's local sweep on this head (Console build, type-check, suites, 45 derived gates) is still running; on 7b3dc299 39 of 45 exited 0 and 6 were stopped for the merge, not failed
accepted deviations none new since the first ACCEPT
out of scope none
landing NOT governed (check-governed-merges --pr 26055: 1013 lines, no hit path); git merge-tree against origin/main (1a298dda+) clean; main drift since 71580254 touches only core, react, data-objectstack (#26292) and components (#26338) records, none of this PR's three. Lines after it: plugin-detail #26277 (seat 1) → #26325; i18n #25921 (seat 1) and #26326

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 26, 2026 23:06
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 26, 2026 23:07
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 0b76a1f Oct 26, 2026
62 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

record activity timeline: fold consecutive same-actor update entries on one record into one row with a count (renderer half of #25787)

2 participants