Skip to content

feat(ui): open field history on the CRM's four main objects; the timeline's Changes chip follows trackHistory - #30158

Closed
objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/issue-30074-crm-track-history
Closed

objectstack-fleet[bot] wants to merge 3 commits into
mainfrom
claude/issue-30074-crm-track-history

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #30074

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么: CRM 示例的线索、客户、联系人、商机四个对象打开了 enable.trackHistory,记录页的动态里能看到每次字段修改的旧值和新值;没有打开它的对象,动态页签上不再出现「变更」筛选。
  • 为什么改: 按裁决 A(6631104393):示例示范正确做法,标志保持按需开启,默认开启不在本次决定。
  • 风险与回滚: 风险低:只改示例对象的一个标志和一个筛选的显示条件,不改数据、不改接口;回滚即撤销本 PR。注意:未开启历史的对象,系统活动里的「更新了…」「创建了…」行原本也落在「变更」下,现在只能在「全部」里看到(见 Acceptance notes)。
  • 先看: ui/packages/plugin-detail/src/renderers/record-activity.tsx(选中的筛选被拿掉时回到「全部」)、ui/packages/plugin-detail/src/recordTimelineStream.ts(新的上下文与筛选列表)、ui/packages/app-shell/src/views/RecordDetailView.tsx(把 historyEnabled 交给动态)。
  • 两分钟验证: pnpm --dir ui exec vitest run packages/plugin-detail/src/__tests__/recordTimelineStream-27055.test.tsx,预期全部通过;或 pnpm dev:crm 后打开一条商机,改赢单概率,再打开动态页签:有「变更」筛选,点开能看到「赢单概率 70 → 75」。
  • 需要你做的: 无,席位落地。

What changes

  • CRM example. crm_lead, crm_account, crm_contact and crm_opportunity declare enable: { trackHistory: true } (none of the four had an enable block). These are the four object entries of the CRM app's Sales navigation other than crm_activity, which is the activity log itself; crm_opportunity_line_item is a child of the opportunity and has no navigation entry. Every other object stays opt-in.
  • Where the Changes chip is drawn. The chip is not drawn by RecordDetailView.tsx: the 动态 stream's chips are rendered by RecordActivityTimeline in @object-ui/plugin-detail, with the chip state held in the record:activity renderer. So the fix lands there, and the host only hands over its gate:
    • @object-ui/plugin-detail: new RecordFieldHistoryContext (boolean, default true, so a host that says nothing keeps every chip) and timelineStreamFilters(fieldHistory). The record:activity stream reads the context; while it is false it offers All / Activity / Discussion only, and a stream left on Changes falls back to All. RecordActivityTimeline takes the chips to draw as streamFilters (all four when absent).
    • @object-ui/app-shell: RecordDetailView provides RecordFieldHistoryContext with its existing historyEnabled (the object declares enable.trackHistory: true and sys_audit_log is registered), the same gate that decides whether it reads the audit log.
  • ui/packages/plugin-detail/README.md says the Changes chip follows the record's field history. Root changeset .changeset/30074-crm-track-history.md (@object-ui/plugin-detail patch; app-shell and the CRM example are private).

File surface: the claim declared the four object files, RecordDetailView.tsx, their tests and one changeset. Outside it, by necessity: four ui/packages/plugin-detail/src files (the chip's real renderer, as the dispatch allowed), the plugin-detail README (ui rule: docs follow code).

Tests

All runs on head 8c5b89c91 (the final commit; nothing pushed after them).

  • New pins, plugin-detail/src/__tests__/recordTimelineStream-27055.test.tsx, a top-level describe in the existing harness: history shown gives four chips; history not shown gives three and All still draws every entry; no provider gives four (control); Changes taken away while chosen leaves the stream on All.
  • New pins, app-shell/src/views/RecordDetailView.recordPageTiers-26678.test.tsx, a top-level describe: a real RecordDetailView mount on ?related=timeline draws all / activity / discussion / changes for a trackHistory object, and all / activity / discussion with zero audit reads for the same object without it.
  • Ablation (one, on the context read in record-activity.tsx, via scripts/ablation-replace.mjs; the subject resolves to src through ui/vitest.config.mts aliases): anchor 1 to 0, blob 7b5fe9176ec0 to 69e1bc36c51c; 3 tests red (the two plugin-detail "not shown" pins and the app-shell "without it" pin), the "shown" and control pins green; restored, blob equals HEAD and git diff HEAD empty.
Test readings
  • Targeted: vitest run of the two pin files plus RecordTimelinePanel-27524.test.tsx: Test Files 3 passed (3), Tests 27 passed (27), lock VERDICT command-exit 0.
  • @object-ui/plugin-detail whole package (pnpm --dir ui exec vitest run packages/plugin-detail/): Test Files 369 passed, 1 skipped (370); Tests 3264 passed, 18 skipped; VERDICT command-exit 0.
  • @object-ui/app-shell, narrowed (declared): the 218 test files that name RecordDetailView, RecordActivityTimeline, recordTimelineStream or record-activity: Test Files 218 passed (218), Tests 1630 passed (1630); VERDICT command-exit 0. The rest of the package is left to CI's Test UI.
  • Type-check: pnpm --filter @object-ui/plugin-detail run type-check exit 0; pnpm --filter @object-ui/app-shell run type-check exit 0 (both run tsc --noEmit and tsc -p tsconfig.test.json).
  • CRM example: pnpm --filter @objectstack/example-crm run typecheck exit 0; its vitest suite Test Files 7 passed (7), Tests 48 passed (48).

Acceptance notes

  • The card's premise that the Changes chip can show nothing is only partly true. Without trackHistory, the stream's Changes chip is not empty: ACTIVITY_TYPE_TO_FEED_TYPE maps sys_activity created / updated / deleted / assigned / shared rows to the field_change feed type, and filterTimelineStream(…, 'changes') keeps them (the existing pin "the four chips" in recordTimelineStream-27055.test.tsx holds an activity created row under Changes). So on an object without history, the "更新了 …:赢单概率 (%)" lines used to be reachable under Changes and are now reachable only under All (the Activity chip leaves field_change out). This PR does what the ruling says; whether such rows should also show under Activity when Changes is hidden is left to the seat and the maintainer.
  • main moved 8 commits past the base 55337f4ce before the PR opened; none touches a path in this diff, so the branch was not re-merged. CI runs the merge ref.
  • Not measured here: a live browser run of the CRM (the pins mount the real RecordDetailView).

Verification

Gates were taken from node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (53 commands, derived at 8c5b89c91).

Gate readings
  • 53 of 53 run, each exit captured before any pipe; --ran reconciliation: "53 derived famil(ies) accounted for — 53 run, 0 NOT-MEASURED (a DERIVED zero — all 53 recorded an exit code and none of them is 3)".
  • node scripts/check-sdui-manifest.mjs exit 0 (seat constraint).
  • pnpm check:dual-build-cjs-loads and pnpm check:i18n-coverage first answered exit 3 (PREREQUISITE NOT MET, builds missing); after their prerequisite builds both exit 0 ("116 published require entry point(s) across 71 package(s) load"; "OK (14 config(s), 426 baselined untranslated string(s), none new)").
  • pnpm check:type-check-debt hit a 400 s local timeout once, then exit 0 under the verify lock.
  • pnpm --dir ui exec vitest run scripts/: Test Files 170 passed, 2 skipped (172); Tests 4985 passed, 2 skipped.
  • Not run locally, CI's: Build Core, Type Check UI and the workspace type-check lanes, Test Core, Test UI (whole), the repo-wide lint.

Generated by Claude Code

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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · os-support-ai · session_01WGufzfE4V9fzZgJLrLDmMC

Closed as superseded by #30294, which resumes this branch's head 8c5b89c9 (Release 6648707864) under ruling C (6649643659) on #30074. The option-A half of this PR is carried over unchanged there; nothing here is lost.

@objectstack-fleet objectstack-fleet Bot closed this Nov 5, 2026
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/m tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Field changes are recorded but not shown unless an object opts in: who decides a record shows its change history?

1 participant