Skip to content

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

Closed

Description

@objectstack-fleet

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

  1. 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.
    • 「+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.
  2. 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.
  3. 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.
  4. Nothing stored changes. record.related.arrangement keeps { v, order, hidden, basedOn, seen }. The partition is computed when the row is drawn, and it is never written back into order.
  5. Dropdown wording. Empty tabs in the dropdown sit after a separator, and their count 0 reads muted.
    • Use the RelatedListEmptyFold tone, 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.

No spec change and no metadata key.

Why (four axes)

File surface

  • ui/packages/plugin-detail/src/RecordPageColumns.tsx: the tabs / hiddenTabs build.
  • ui/packages/plugin-detail/src/RecordTabRow.tsx and src/synth/recordTabRow.ts: an "always in the dropdown" group, beside alwaysInDropdown.
  • Tests beside, and a changeset.
  • ⛔ packages/spec/**. ⛔ The stored arrangement shape.

Acceptance

  1. 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 null count stays in place.
    • It also pins that an active empty tab is on the row.
    • Ablation: revert the partition, and the test fails.
  2. A component test: a page whose counts arrive with the payload draws the final order on the first render. No tab moves after.
  3. 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.

Activity

  1. added
    enhancementNew feature or request
    priority:p1High: required for production / M2
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    on Oct 31, 2026
  2. objectstack-fleet commented on Oct 31, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: audit · p1 · domain:ui · agreed · 2026-10-31T20:55Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-tesla · session_01L9qibw2uhdSeHYfnZdAf4V

    Claim: PM loop round 2 · 2026-11-01T03:53Z
    Session: session_01L9qibw2uhdSeHYfnZdAf4V (seat; implementation by card thread session session_01L9qibxZzW2JM4cGXJPEJK6, mode:thread)
    Account: os-tesla (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/project-thread-smdw89
    Worktree: objectstack-issue-28469
    Domain: domain:ui
    Seat: domain:ui#1
    File 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: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-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 on UnifiedSidebar.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.

  4. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    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)."
     ]
    }
  5. objectstack-fleet commented on Nov 1, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#1 · os-tesla · session_01L9qibw2uhdSeHYfnZdAf4V

    Landed · 2026-11-01T06:49Z · PR #28538 merged through the merge queue as 310cb96d at 2026-11-01T06:49:07Z. Seat ACCEPT 6554496799 (a452af75).

    probe before (310cb96d~1) now (origin/main)
    export function partitionRecordTabsByRecords in plugin-detail/src/synth/recordTabRow.ts 0 1
    data-record-tab-empty in plugin-detail/src/RecordTabRow.tsx 0 1

    The card closed on Fixes, and pm:dispatched and the assignee are removed in the same stroke as this note.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:recordsBusiness objects, records, the views that show data, usable forms, searchdomain:uienhancementNew feature or requestpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions