Repository navigation
fix(ui): one record action reads as one timeline row with its field changes; every timeline peek row names its actor - #30342
Merged
Conversation
crm_lead, crm_account, crm_contact and crm_opportunity declare enable.trackHistory, so their record pages show each field change with its before and after values. Claude-Session: https://claude.ai/code/session_01QSeKYnE9YNkSZen7sijLA3 Co-authored-by: Claude <noreply@anthropic.com>
… is shown RecordDetailView hands its trackHistory gate (historyEnabled) to the stream through RecordFieldHistoryContext; record:activity drops the changes chip while it is false and falls back to all when the chosen chip goes away. Claude-Session: https://claude.ai/code/session_01QSeKYnE9YNkSZen7sijLA3 Co-authored-by: Claude <noreply@anthropic.com>
…ory; changeset Claude-Session: https://claude.ai/code/session_01QSeKYnE9YNkSZen7sijLA3 Co-authored-by: Claude <noreply@anthropic.com>
… only; activity events sit under Activity Ruling C on the record timeline: an item is a field-history entry when it is a field_change read from the audit log (source 'sys_audit_log'). The stream's Changes chip and the panel's changes_only hold those and nothing else; the activity rows that share the field_change presentation (created, updated, deleted, assigned, shared) sit under Activity whatever the history flag. ACTIVITY_TYPE_TO_FEED_TYPE is unchanged. Claude-Session: https://claude.ai/code/session_01WGufzic59DvGAbu7U78pZt Co-authored-by: Claude <noreply@anthropic.com>
…nd README follow ruling C Claude-Session: https://claude.ai/code/session_01WGufzic59DvGAbu7U78pZt Co-authored-by: Claude <noreply@anthropic.com>
…y peek row names its actor An activity row and the audit entry the same action wrote (same record, matching action kind, same actor, stamped within TIMELINE_PAIR_WINDOW_MS) merge into one stream row: the activity row keeps its actor and summary and carries the audit entry as fieldHistory, drawn as the before and after values on that row. Changes and the panel changes_only still show each audit entry on its own. Unpaired rows draw as before. The panel peek row shows the actor. Claude-Session: https://claude.ai/code/session_01WGufzic59DvGAbu7U78pZt Co-authored-by: Claude <noreply@anthropic.com>
…nd README for one row per action Claude-Session: https://claude.ai/code/session_01WGufzic59DvGAbu7U78pZt Co-authored-by: Claude <noreply@anthropic.com>
Contributor
Author
|
domain:ui#1 · os-support-ai · session_01WGufzfE4V9fzZgJLrLDmMC 维护者速读(席位终稿)
席位核对 · ACCEPT · head
|
This was referenced Nov 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Yibo Wang · project thread
Fixes #30122
Clause-②: no
Metadata-protocol: none
维护者速读
ACTIVITY_TYPE_TO_FEED_TYPE未动,裁决 C(「变更」只放审计条目)不变。配对规则:同一记录、动作对应(create↔created、update↔updated、delete↔deleted)、同一操作人、时间相差不超过 5 秒,就近配对;配不上的照旧各自一行。回滚即撤销本 PR。注意:页签上的「动态 N」数字来自服务端计数,新建一条有字段历史的记录后仍可能比显示的行数多 1,要等另一张卡改 spec 与 REST。ui/packages/plugin-detail/src/recordTimelineStream.ts(pairActions与mergeTimelineStream)、ui/packages/plugin-detail/src/RecordActivityTimeline.tsx(合并行的显示与预览行的操作人)、ui/packages/plugin-detail/src/__tests__/recordTimelineStream-27055.test.tsx(新 pin)。pnpm --dir ui exec vitest run packages/plugin-detail/src/__tests__/recordTimelineStream-27055.test.tsx,预期全部通过;或pnpm dev:crm后新建一条线索、改公司名,打开动态页签:「全部」里只有两行(创建了、更新了),每行下面有改前→改后。What changes
mergeTimelineStream(recordTimelineStream.ts) now pairs an activity row with the audit entry the same action wrote, and draws them as ONE row: the activity row, carrying the audit entry asfieldHistory(a package-local hand-off type,TimelineStreamItem). The row keeps its actor and summary;RecordActivityTimelinedraws the audit entry's before and after values on a second line of the same row (data-stream-change-diff).pairActions,pairGap): same record (both sources are read for this one record); matching action kind, auditcreate/update/deleteto activitycreated/updated/deleted; same actor (the audit'suser_idequals the activity'sactor_id; where neither names a user, the same display name); stamps no more thanTIMELINE_PAIR_WINDOW_MS= 5000 ms apart. Candidates pair nearest in time first, each row and each entry once. Measured on a live CRM boot: activitytimestampand auditcreated_atof one create, and of one update, were 1 ms apart, with the same user id; plugin-audit writes both in one call.auditEntriesToFeedItemscarries the ledger'sactionanduser_idonto its items for this; the host already spreaduser_idinto its enriched entries, soRecordDetailViewis unchanged.updatedactivity row still steps aside (the old blanket rule, now applied only to rows that did not pair).changes_only, show each audit entry on its own (fieldHistoryEntriesOf). A reader without audit access gets no field changes from the host, so nothing pairs and the activity row draws alone, as before.main(and did on987ff33b88); full comment rows do too. The one face without a visible actor was the 动态 panel's peek row (actor only in the hover title). It now shows the actor beside the time (data-record-timeline-peek-actor)..changeset/30122-timeline-one-row-per-action.md(@object-ui/plugin-detailpatch); plugin-detail README says one action is one row.Tests
All readings on head
0b041cec, the final commit.recordTimelineStream-27055.test.tsx): create pairs into one row and 变更 still shows the audit entry alone; update pairs; unpaired audit and unpaired activity rows stay two rows; different actors do not pair; one past the window does not pair and the window's edge does; a different action kind does not pair; two entries for one row pair nearest first; the system on both sides pairs by name.changes_only).scripts/ablation-replace.mjsonrecordTimelineStream.ts, anchorconst pairs = pairActions(activity, changes);replaced by an empty map (the subject resolves tosrcthrough theui/vitest.config.mtsaliases, no dist leg): anchor 1 to 0, bloba406f0a324batob2e7fb7be1e8; 6 tests red, exactly the six pins that need a pair (create, update, window edge, nearest first, system by name, the audit reader's one row); the unpaired, different-actor, non-audit-reader, 变更 and peek pins green. Restored: blob equals HEADa406f0a324ba,git diff HEADempty.objectstack dev --seed-admin --fresh, Vite console, zh-CN). As admin: create a lead, then change its company. 全部 draws two rows: 「更新了 线索 "Live after Lead":公司 · Dev Admin · 刚刚」 with 「公司 Pair Co → Pair Holdings」 under it, and 「创建了 线索 "Live after Lead" · Dev Admin · 刚刚」 with the initial values under it; 变更 shows the two audit entries on their own; the peek rows read summary, actor, time. As a member grantedcrm_sales_userplus a scratch set readingsys_activityandsys_comment(theirsys_audit_logread answers 403PERMISSION_DENIED): 全部 draws the same two rows with no diff, peek rows name the actor. Screenshots are kept in the project files, not committed.Test readings
0b041cec).@object-ui/plugin-detailwhole package plusRecordDetailView.recordPageTiers-26678.test.tsx: Test Files 370 passed, 1 skipped (371); Tests 3303 passed, 18 skipped (3321); lock VERDICT command-exit 0.@object-ui/plugin-detailexit 0;@object-ui/app-shellexit 0 after a full build (bothtsc --noEmitandtsc -p tsconfig.test.json).Acceptance notes
timelinecount (packages/rest/src/record-page-routes.ts,answerTimeline: comments + audit writes + drawn activity, minusupdatedrows when any field change exists), whose formula the spec spells as an upper bound (packages/spec/src/api/record-page.zod.ts, theRecordPageTimelineRequestSchematable andRECORD_TIMELINE_UPDATE_ACTIVITY_TYPE). Live: one create plus one update read 「动态 3」 for the admin over two drawn rows (the member, who counts no audit, read 「动态 2」). Counts the client makes itself (the stream's own header count and the count probe where the host has no total) now count one per action. Options for the follow-up card: (A) letcreatedanddeletedstep aside too, besideupdated, when any field change is counted, in the spec table, its.describe()text andanswerTimeline; (B) count only activity rows when both sources are on and ignore audit writes, dropping unpaired audit entries from the count; (C) leave the upper bound. The seat files it as its own card.historyEnabledreadsenable.trackHistoryand whethersys_audit_logis registered, not whether this reader may read it (live: the member's 变更 was empty). Not changed here.recordTimelineStreamorRecordActivityTimeline(search at PR time: 0 and 0).Verification
Gates from
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, derived at0b041cec(35 commands).Gate readings
--ran: "35 derived famil(ies) accounted for — 35 run, 0 NOT-MEASURED (a DERIVED zero — all 35 recorded an exit code and none of them is 3)".node scripts/check-sdui-manifest.mjsexit 0;pnpm --dir ui exec vitest run scripts/: Test Files 170 passed, 2 skipped (172); Tests 4985 passed, 2 skipped.pnpm --dir ui run check:new-line-citations: 0 new citations, exit 0.check-platform-checklistnot owed: nothing renamed or removed (new module members only; none added to the package entry).mainmoved 1 commit after the merge base by PR time; it touches no path in this diff andgit merge-treeis clean. CI runs the merge ref.Generated by Claude Code