You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit ff3f83b
Browse filesBrowse the repository at this point in the historyBrowse files
fix(ui): one record action reads as one timeline row with its field changes; every timeline peek row names its actor (#30342)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBVmFoQ6NLiPf5VfPaWXaynQ)_Fixes#30122
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么:** 记录动态里,一个操作同时写下的「活动记录」和「审计记录」现在合成一行:这一行保留谁做的和做了什么(「更新了 线索…:公司
· Dev
Admin」),下面直接带上改前→改后的值;看不到审计日志的成员看到的也是同一行,只是没有改前改后值。右栏动态预览的每一行也显示操作人了。
- **为什么改:**
卡片的期望是「每行都说清是谁;一个操作对所有人都只是一行,有权限的人在这一行上看到字段差异」。实测:流内紧凑行早已显示操作人,缺的只是右栏预览行;重复两行是真的(新建一条线索,管理员看到「创建了…」和一长串初始值两行)。
- **风险与回滚:** 风险低:只改 plugin-detail
的前端合并与显示,不改数据、接口、spec,`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
- **One action, one row.** `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 as `fieldHistory` (a package-local
hand-off type, `TimelineStreamItem`). The row keeps its actor and
summary; `RecordActivityTimeline` draws the audit entry's before and
after values on a second line of the same row
(`data-stream-change-diff`).
- **The pairing rule** (`pairActions`, `pairGap`): same record (both
sources are read for this one record); matching action kind, audit
`create` / `update` / `delete` to activity `created` / `updated` /
`deleted`; same actor (the audit's `user_id` equals the activity's
`actor_id`; where neither names a user, the same display name); stamps
no more than `TIMELINE_PAIR_WINDOW_MS` = 5000 ms apart. Candidates pair
nearest in time first, each row and each entry once. Measured on a live
CRM boot: activity `timestamp` and audit `created_at` of one create, and
of one update, were 1 ms apart, with the same user id; plugin-audit
writes both in one call. `auditEntriesToFeedItems` carries the ledger's
`action` and `user_id` onto its items for this; the host already spread
`user_id` into its enriched entries, so `RecordDetailView` is unchanged.
- **Unpaired rows draw as before**: an audit entry with no activity row
is its own row, and with field changes present an unpaired `updated`
activity row still steps aside (the old blanket rule, now applied only
to rows that did not pair).
- **Chips (ruling C on #30074 unchanged).** 全部 and 活动 show the paired
row with its diff; 变更, and the panel's `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.
- **Actor.** Measured first: the stream's compact rows already name the
actor on `main` (and did on `987ff33b88`); 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 `.changeset/30122-timeline-one-row-per-action.md`
(`@object-ui/plugin-detail` patch); plugin-detail README says one action
is one row.
## Tests
All readings on head `0b041cec`, the final commit.
- **Pairing pins** (new top-level describe in
`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.
- **Renderer pins:** an audit reader sees ONE compact row with the
summary, the actor and the diff line; a reader without the audit log
sees the same one row with no diff line; 变更 shows the audit diff on its
own row; every peek row names its actor.
- The ruling C pins from #30294 are unchanged and green (the four chips,
activity under 活动 with history on and off, panel `changes_only`).
- **Ablation** via `scripts/ablation-replace.mjs` on
`recordTimelineStream.ts`, anchor `const pairs = pairActions(activity,
changes);` replaced by an empty map (the subject resolves to `src`
through the `ui/vitest.config.mts` aliases, no dist leg): anchor 1 to 0,
blob `a406f0a324ba` to `b2e7fb7be1e8`; 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 HEAD
`a406f0a324ba`, `git diff HEAD` empty.
- **Live** (example CRM, `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 granted `crm_sales_user` plus a scratch set reading
`sys_activity` and `sys_comment` (their `sys_audit_log` read answers 403
`PERMISSION_DENIED`): 全部 draws the same two rows with no diff, peek rows
name the actor. Screenshots are kept in the project files, not
committed.
<details><summary>Test readings</summary>
- Targeted, the three timeline test files: Test Files 3 passed (3),
Tests 80 passed (80) after the nearest-first fix (the first run caught a
greedy-order pairing: 1 red, fixed in `0b041cec`).
- `@object-ui/plugin-detail` whole package plus
`RecordDetailView.recordPageTiers-26678.test.tsx`: Test Files 370
passed, 1 skipped (371); Tests 3303 passed, 18 skipped (3321); lock
VERDICT command-exit 0.
- Type-check: `@object-ui/plugin-detail` exit 0; `@object-ui/app-shell`
exit 0 after a full build (both `tsc --noEmit` and `tsc -p
tsconfig.test.json`).
- Ablation run: Test Files 1 failed, 2 passed (3); Tests 6 failed, 74
passed (80).
</details>
## Acceptance notes
- **The 动态 N badge can still read one more than the rows drawn.** The
tab badge, the panel's 「查看全部 N 条」 and the peek total read the server's
`timeline` count (`packages/rest/src/record-page-routes.ts`,
`answerTimeline`: comments + audit writes + drawn activity, minus
`updated` rows when any field change exists), whose formula the spec
spells as an upper bound (`packages/spec/src/api/record-page.zod.ts`,
the `RecordPageTimelineRequestSchema` table and
`RECORD_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) let
`created` and `deleted` step aside too, beside `updated`, when any field
change is counted, in the spec table, its `.describe()` text and
`answerTimeline`; (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.
- **A reader without audit access still sees an empty 变更 chip** on a
history-on object: the host's `historyEnabled` reads
`enable.trackHistory` and whether `sys_audit_log` is registered, not
whether this reader may read it (live: the member's 变更 was empty). Not
changed here.
- Window choice: 5 s is far above the 1 ms measured and allows a driver
that keeps whole seconds; two different actions by the same user on the
same record within 5 s of each other still pair correctly because
pairing also needs the matching kind and goes nearest first.
- No other open PR names `recordTimelineStream` or
`RecordActivityTimeline` (search at PR time: 0 and 0).
## Verification
Gates from `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, derived at `0b041cec` (35 commands).
<details><summary>Gate readings</summary>
- 35 of 35 run serially after a full build, each exit code captured
before any pipe: all 35 exit 0. `--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.mjs` exit 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-checklist` not owed: nothing renamed or removed (new
module members only; none added to the package entry).
- `main` moved 1 commit after the merge base by PR time; it touches no
path in this diff and `git merge-tree` is clean. CI runs the merge ref.
- Not run locally, CI's: Build Core, Type Check UI and the workspace
type-check lanes, Test Core, Test UI (whole app-shell), the repo-wide
lint.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzic59DvGAbu7U78pZt)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(record page): one action reads as one timeline row, and every row of the timeline panel names who acted
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
-**One row per action.** When one action wrote both an activity row and an audit-log entry (same record, matching kind: create and created, update and updated, delete and deleted; same actor; stamped within `TIMELINE_PAIR_WINDOW_MS`, 5 seconds), the record page's timeline draws it once: the activity row keeps its actor and summary and shows the field changes, before and after, on the same row. A reader who cannot read the audit log sees the same single row without them. The Changes chip, and the panel's `changes_only`, still show each audit entry on its own. Rows that pair with nothing draw as before.
11
+
-**The 动态 panel's peek rows name the actor**, beside the time.
12
+
- The record page's 动态 count still comes from the server's `timeline` count, which the spec defines as an upper bound: for a record created with field history on it can still read one more than the rows drawn.
0 commit comments