Repository navigation
Record page tab row: tabs of related lists with records come first, and the tabs of empty related lists sink into the 「+N」 dropdown, the rule the right column already applies to empty panels #28469
Copy link
Copy link
Closed
Labels
area:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchdomain:uienhancementNew feature or requestNew feature or requestpriority:p1High: required for production / M2High: required for production / M2
Description
epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff
Filed by the record-page epic seat on the maintainer's instruction. The seat walked the v4 record page on main against HotCRM metadata (2026-10-31), and the maintainer answered 「两条都立:页签顺序(有数据优先、空关联沉到 +N)…,都开在 domain:ui」 (2026-11-01). Part of the record-page epic #26676; it follows #27524, which is closed.
Reading
Reading at main 52306d40bd. The page was observed on 5da3a8ce31 with the HotCRM metadata run by main's CLI. PR #28458 (④) landed after that, and it does not touch the order.
- The rows.
RecordPageColumns.tsx(around lines 352–371) builds the tab row as follows:- 概览 first.
- 动态 second.
- Then one tab per panel, in
resolveRelatedArrangement(...).order. - The hidden panels' tabs go at the end of the dropdown.
- What a tab's count does. A tab's count is only a label. It never moves the tab.
- The overflow.
RecordTabRow(layoutRecordTabRow,synth/recordTabRow.ts) puts as many tabs on the row as fit. The rest go into 「+N ▾」. - The right column is different. It already applies Record page v4: same-shaped related panels with no collapse, a persistent tab row under the header (the enlarged state is the active tab), Activity as the first panel and a tab, one top bar on every face, tiers by container width #27524 Target 2: empty panels fold into one line, 「还有 N 个空关联 · … · 展开」, and non-empty panels keep their order. The tab row does not apply that rule.
- What the reader sees. HotCRM contact John Smith, in the drawer at 800 px wide:
- The row shows 概览 · 动态 0 · 活动 1 · 活动参与者 7 · 线索 · 转化联系人 0 · 线索 · 重复的联系人 0.
- 「+6」 holds 商机 0 · 任务 0 · 工单 2 · 合同 1 · 报价单 2 · 营销活动成员 3.
- Two empty lists take visible slots. Four lists with records are hidden.
- The same record's overview says 「还有 4 个空关联」 in the right column, so the two surfaces disagree about the same lists.
Target
- One order rule for the tab row and the panels.
- 概览 and 动态 stay first and second.
- The related panels follow, as a stable partition of the arrangement order:
- First, every panel whose count is not
0. They keep the arrangement's relative order, so a reader's drag still decides the order among them. - Then every panel whose count is
0, in arrangement order.
- First, every panel whose count is not
- 「+N ▾」 lists the overflowed non-empty tabs first, then the empty tabs, then the hidden panels' tabs, as now.
- The width computation is unchanged. Empty tabs are always placed in the dropdown, even when the row has room for them. An empty list never takes a slot from a list with records.
- A count that is not known yet (loading, or refused) counts as non-empty: the tab stays where the arrangement puts it. The counts come with the record's page payload (
POST /data/:object/:id/page, Seam: one request opens a record — a record-page payload (record + related counts and first rows + field access + lock) and an app-scoped metadata bundle, so a record open is 2–3 requests instead of 23–45 serialized in three stages #27627). So on a normal open the order is final at first paint. Measure that: a tab must not move after the row first draws. - The active tab stays visible, as Target 8 of Record page v4: same-shaped related panels with no collapse, a persistent tab row under the header (the enlarged state is the active tab), Activity as the first panel and a tab, one top bar on every face, tiers by container width #27524 rules. A deep link (
?related=) to an empty list still shows that tab on the row, in the last visible slot. - Nothing stored changes.
record.related.arrangementkeeps{ v, order, hidden, basedOn, seen }. The partition is computed when the row is drawn, and it is never written back intoorder. - Dropdown wording. Empty tabs in the dropdown sit after a separator, and their count
0reads muted.- Use the
RelatedListEmptyFoldtone, so the two folds read alike. - No new i18n key is needed unless the separator carries a label. If one is added, add it in all ten packs.
- Use the
No spec change and no metadata key.
Why (four axes)
- Business need: found by the epic seat on HotCRM's real metadata. A contact has 12 inbound related lists. The lists with records (cases, contracts, quotes) were the ones a reader could not see without opening a menu.
- Long-term fit: the right column and the tab row describe the same lists, so one rule for both. Record page v4: same-shaped related panels with no collapse, a persistent tab row under the header (the enlarged state is the active tab), Activity as the first panel and a tab, one top bar on every face, tiers by container width #27524 Target 2 already ruled that empty lists fold; this card extends that rule to the tab row. The reader's arrangement still decides the order, so Target 10's stored shape is untouched.
- AI-error resistance: the order comes from data, not from a new declaration. An app author has nothing to tune and nothing to get wrong.
- Startup focus: a pure rendering rule on existing data. It adds no setting and no "sort tabs by" option.
File surface
ui/packages/plugin-detail/src/RecordPageColumns.tsx: thetabs/hiddenTabsbuild.ui/packages/plugin-detail/src/RecordTabRow.tsxandsrc/synth/recordTabRow.ts: an "always in the dropdown" group, besidealwaysInDropdown.- Tests beside, and a changeset.
- ⛔
packages/spec/**. ⛔ The stored arrangement shape.
Acceptance
- A unit test on the layout: given counts
[7, 0, 0, 2, 1, 2, 3]in arrangement order, the row shows the non-empty tabs first, and the dropdown lists the overflowed non-empty tabs, then the empty ones, then the hidden ones.- It also pins that a
nullcount stays in place. - It also pins that an active empty tab is on the row.
- Ablation: revert the partition, and the test fails.
- It also pins that a
- A component test: a page whose counts arrive with the payload draws the final order on the first render. No tab moves after.
- Screenshots on an evidence branch: HotCRM contact John Smith in the drawer at 800 px and on the page at 1440 px, before and after. Cases, contracts and quotes are on the row; the two empty 线索 lists are in the dropdown.
Metadata
Metadata
Assignees
Labels
area:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchdomain:uienhancementNew feature or requestNew feature or requestpriority:p1High: required for production / M2High: required for production / M2
Activity
objectstack-fleet commentedon Oct 31, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p1 ·
domain:ui· agreed · 2026-10-31T20:55Z⛔ Not a claim.
pm:*label, so the epic seat dispatches it. The tab row applies the right column's existing Record page v4: same-shaped related panels with no collapse, a persistent tab row under the header (the enlarged state is the active tab), Activity as the first panel and a tab, one top bar on every face, tiers by container width #27524 Target 2 rule (empty panels fold), with no new key.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:ui#1 · os-tesla · session_01L9qibw2uhdSeHYfnZdAf4V
Claim: PM loop round 2 · 2026-11-01T03:53Z
Session:
session_01L9qibw2uhdSeHYfnZdAf4V(seat; implementation by card thread sessionsession_01L9qibxZzW2JM4cGXJPEJK6,mode:thread)Account:
os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-smdw89Worktree:
objectstack-issue-28469Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/plugin-detail/src/{RecordPageColumns.tsx,RecordTabRow.tsx,synth/recordTabRow.ts}and their tests, i18n catalogs only if the separator carries a label (all ten packs), root changeset; ⛔packages/spec/**, ⛔ the stored arrangement shape (stop on breach; explain in the report)Container & model:
M,mode:thread,model: default(dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: none
Premise: holds
Serial constraints cleared: sibling #28484 may edit
plugin-detail/src/RelatedList.tsx(mobile enlarged face only), a different file; #28497 runs onUnifiedSidebar.tsx.Order: /mnt/project-files/ui-seat-1/r2026-10-31b/dispatch-28469.md, under the maintainer's 2026-11-01T03:47Z 「任务有一大堆啊,都可以派,包括 p3」.
Takeover: the record-page epic seat (
epic:#26676#1) filed this card 2026-10-31T20:39Z and triage agreed it at 2026-10-31T20:55Z for that seat to dispatch; no claim, branch or PR since. The seat takes it under the maintainer's standing two-hour rule (2026-10-23T15:42Z) and his 2026-11-01T03:47Z word above.objectstack-fleet commentedon Nov 1, 2026
domain:ui#1 · os-tesla · session_01L9qibxZzW2JM4cGXJPEJK6
os-dev-report
{ "issue": 28469, "status": "done", "branch": "claude/project-thread-smdw89", "pr": "https://github.com/objectstack-ai/objectstack/pull/28538", "session": "session_01L9qibxZzW2JM4cGXJPEJK6", "premise_still_valid": true, "summary": "The record tab row partitions the related panels stably: lists with a count other than 0 keep the arrangement order on the row, lists with count 0 always sit in the dropdown after the overflowed tabs, behind a separator in a muted face, before the hidden ones; an active empty tab takes the row's last slot; nothing stored changes. The PM mechanism assumption (counts known at first paint) measured false on the live Console, so by the seat's ruling the panel tabs wait off the row while the first count set is pending and appear once in final order; the active tab is always drawn and an uncounted list keeps its place.", "tests": "head 13333cdf: pnpm --dir ui exec vitest run packages/plugin-detail/ -> 359 files passed, 1 skipped; 3185 tests passed, 18 skipped (lock VERDICT command-exit 0). New: synth/__tests__/recordTabRow-28469.test.ts (6), __tests__/RecordTabRow.recordsFirst-28469.test.tsx (7); updated RecordPageColumns.tabRow-27524.test.tsx (count-0 tab now in dropdown). type-check (tsc --noEmit && tsc -p tsconfig.test.json) exit 0 after env -u OS_SKIP_DTS build of plugin-detail^... . Ablations via scripts/ablation-replace.mjs, restore blob verified each time: partition reverted -> 9 red; active-empty rule removed -> 2 red; pending hold removed -> 2 red; active exception removed from hold -> 1 red. Sources are imported relatively by the tests, so no dist leg.", "gates": "dispatch-gates --commands at 13333cdf derived 34 families; all 34 run locally with exit 0 (list: ui-seat-1/r2026-10-31b/out/28469-gates-ran-13333cdf.list); --ran: 34 derived, 34 run, 0 NOT-MEASURED. Extra: check:new-line-citations 0 new; eslint on the 6 changed files 0 errors. CI check-runs: in_progress at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "3 — mcp__github__issue_read x3 (get x1, get_comments x2); 0 write tools", "api_writes": "2 — relay pr_create (objectstack-ai/objectstack#28538, read-back identical, 0 assignees); this os-dev-report comment via post-stamped", "line_budget": "538 changed lines (+520 / -18, 7 files) vs 3000: under", "deviations": [ "Target 2 implemented by its intent (pending tabs held off the row), not its literal rule; measured tab-row log in the PR body; route agreed by the seat 2026-11-01T04:51Z.", "Acceptance 3 shots on examples/app-crm (page 1440 px, page 800 px, list drawer 1440 px), not HotCRM, as the order allows; in project folder ui-seat-1/r2026-10-31b/out/28469-shots/, not on an evidence branch (card threads push only their thread branch)." ], "files_changed": [ ".changeset/28469-record-tab-row-records-first.md", "ui/packages/plugin-detail/src/RecordPageColumns.tsx", "ui/packages/plugin-detail/src/RecordTabRow.tsx", "ui/packages/plugin-detail/src/synth/recordTabRow.ts", "ui/packages/plugin-detail/src/synth/__tests__/recordTabRow-28469.test.ts", "ui/packages/plugin-detail/src/__tests__/RecordTabRow.recordsFirst-28469.test.tsx", "ui/packages/plugin-detail/src/__tests__/RecordPageColumns.tabRow-27524.test.tsx" ], "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed — the record page draws its frame before the POST /page payload lands, so related counts are pending at first paint (seat ruled: no separate card)." ] }objectstack-fleet commentedon Nov 1, 2026
domain:ui#1 · os-tesla · session_01L9qibw2uhdSeHYfnZdAf4V
Landed · 2026-11-01T06:49Z · PR #28538 merged through the merge queue as
310cb96dat 2026-11-01T06:49:07Z. Seat ACCEPT 6554496799 (a452af75).310cb96d~1)origin/main)export function partitionRecordTabsByRecordsinplugin-detail/src/synth/recordTabRow.tsdata-record-tab-emptyinplugin-detail/src/RecordTabRow.tsxThe card closed on
Fixes, andpm:dispatchedand the assignee are removed in the same stroke as this note.