Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .changeset/806-detail-sections-group-ref.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
'hotcrm': patch
---

The Details tab of the lead, opportunity and case record pages now shows the object's own field groups

Each of the three record pages used to list its own hand-picked sections, and
those sections had drifted from the field groups the object declares. Fields
that existed on the record were missing from the page. Each section now points
at one of the object's field groups, so the Details tab shows the same groups,
with the same headings and icons, as the rest of the app. A field added to a
group appears on the record page too.

**Leads.** The Details tab now shows Identity, Company Information, Contact
Information, Communication Preferences, Qualification, Assignment, Address,
Conversion, Duplicate Management and Additional Info. These fields could not be
seen on the Details tab before and now can: **Notes**, **Do Not Call**, **Email
Opt Out**, **Next Follow-up Date**, **Last Contacted**, **Need Type**,
**Estimated Amount**, **Conversion Approval** and the conversion result
(**Converted**, the converted account, contact and opportunity, and the
conversion date). **Description** is now inside **Additional Info**, together
with Notes. Address, Additional Info, Communication Preferences, Conversion and
Duplicate Management start collapsed: click the heading to open them.

**Opportunities.** The Details tab now shows Basic Information, Financials,
Classification, Campaigns, Sales Process, Forecast & Metrics and Notes & Next
Steps. Newly visible: **Primary Contact**, **Stage Entry Date**, **Approval
Status**, **Approved Date**, **Win Reason**, **Loss Reason**, **Loss/Win
Details**, **Days in Current Stage** and **Private**. Campaigns and Forecast &
Metrics start collapsed. The Campaigns group appears once the opportunity has a
campaign; set it from **Edit**.

**Cases.** The Details tab now shows Case Information, Origin & Routing, SLA &
Priority, Escalation, Resolution and System. **Escalated Date** is shown for
the first time, under Escalation. **First Response Date** and **Resolved by
Article** are also new. Internal Notes moved to the System group. Escalation
and System start collapsed. SLA & Priority and Resolution appear once the case
has a value in them, for example when **Close Case** records the resolution.

Fields already shown in the strip at the top of the page (status, owner,
amount, priority and similar) still appear only there.
207 changes: 52 additions & 155 deletions src/sales/pages/lead_detail.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,165 +350,62 @@ export const LeadDetailPage: Page = {
columns: '2',
// `layout` was REMOVED from `record:details` in
// @objectstack/spec 17.0.0 (#6946, ADR-0087 D2) and is
// deleted with no successor: its declared `auto` |
// `custom` semantics were never implemented. objectui's
// `RecordDetailsRenderer` tests `layout` only against
// `inline` | `compact` — two values the enum never
// permitted — so both legal values took the same branch
// and the key selected nothing. The body is chosen by what
// is authored — and on an authored `record:details` the
// `sections` below ARE the body, not a preference over
// some default one.
// deleted with no successor: its `auto` | `custom`
// semantics were never implemented.
//
// ⛔ There is NO fallback. Omitting `sections` here does
// not fall back to the object's `highlightFields`, nor to
// its `fieldGroups`, nor to a bare auto-detected header
// chip. It renders an EMPTY body.
// Every section is a GROUP REFERENCE (#806, maintainer
// ruling C, decision batch #21, 2026-09-03). `{ group }`
// names one of crm_lead's `fieldGroups`, and the renderer
// derives that section's members, label, icon and collapse
// state from the object (`deriveFieldGroupLayout`,
// ADR-0085 §5). Membership has ONE declaration site — the
// `group:` on each field in lead.object.ts — and this page
// curates only the ORDER, so a field added to a group shows
// up here with no edit to this file. ⛔ Do not enumerate
// `fields:` again, and do not restate a key the group owns
// (`name` / `label` / `icon` / `collapsible` /
// `defaultCollapsed`) beside `group`: the spec refuses both.
//
// ⚠️ Read that twice before editing, because both of the
// wrong answers were once written down as fact. THIS
// comment used to assert the `highlightFields` fallback;
// a `crm_forecast` reading recorded on #1452 asserted the
// `fieldGroups` one. Both read as authoritative, they
// contradicted each other, and neither had ever been run
// against the pinned version — so #806's ruling was
// written on a mechanism that does not exist, and that
// cost a full round before anybody measured it. #1521 is
// the card that replaced both guesses with the numbers
// below. ⛔ Do not restore a fallback claim here without
// re-measuring it first.
// ⛔ `sections` stays load-bearing. Omitting it renders an
// EMPTY Details tab, not a `fieldGroups` fallback (#806 R28
// on 17.2.0, #1521); derivation is per section, through
// `group:`, never page-level.
//
// How it was measured (#806's R28 os-dev report) —
// measured, not inferred: headless Chromium driving a
// real `objectstack start` against a wiped DB,
// `@objectstack/console` 17.2.0, two full runs over the
// same records. Run A, unmutated: 6 sections, 20 field
// rows. Run B, `properties.sections` deleted: 0 and 0 —
// the entire body between the tab strip and the
// "Created by" footer simply absent. Negative control in
// both runs: `crm_contact`, which authors no record page,
// kept rendering its five `fieldGroups`-derived headings
// in A and in B alike, so run B's nothing is this page's
// nothing and not a dead instrument.
//
// Why the two wrong answers looked right: `fieldGroups`
// derivation is real, but it lives in the console's page
// SYNTHESIZER — the path that fabricates a record page
// for an object that has none authored (which is exactly
// why the `crm_contact` control shows it). The synthesizer
// takes authored sections when they are non-empty and
// derives from `fieldGroups` otherwise. The
// `record:details` RENDERER, which is what draws THIS
// page, reads neither `fieldGroups` nor `highlightFields`
// at all: it forwards `sections`/`fields`, and the detail
// view guards each one with `.length > 0` and no else
// branch. An authored page opts out of the synthesizer,
// so it opts out of the derivation with it.
//
// ⚠️ Version caveat: those browser numbers are 17.2.0,
// two and more pins ago (17.4.0 came with PR #1814, then
// 17.5.0, then 17.6.0). #1521 did
// NOT re-run the
// browser measurement. What it did do is read the
// 17.3.0 console bundle (static, not run) — a pin that is
// itself now behind, and nobody has re-read the 17.4.0,
// 17.5.0 or 17.6.0 bundle — and
// the mechanism is unchanged there: both guards are still
// `sections.length > 0` and `fields.length > 0` with no
// else, the renderer still reads neither of the two
// fallback sources, and the synthesizer still hands the
// derived `highlightFields` to the details node as
// `hideFields` — highlight fields are SUBTRACTED from
// this body, never substituted into it, which is the
// opposite of what the old sentence claimed. Treat the
// 0/0 as a 17.2.0 reading corroborated statically at
// 17.3.0; re-measure in a browser before quoting it for
// any later version. (`fieldGroups` does reach a section
// one way, per the installed spec: a section may name
// `group:` to inherit one group's members and
// presentation. That is a per-section opt-in, not a
// page-level fallback, and this page does not use it.)
//
// So the sections below are load-bearing, not decorative:
// they are the Salesforce-style structured field grid the
// Details tab presents, and with them gone the tab draws
// nothing at all. ⛔ Whether this page should keep
// authoring them is #806's subject and a maintainer
// decision — not a drive-by edit from here. Field names
// map to lead.object.ts.
// Measured on @objectstack/console 17.6.0 (#806 R70,
// headless Chromium, with a control leg; on 17.4.0 R61
// measured this form crashing the component). What a
// group section does there is the renderer's, not ours:
// - it lays out its own column count (the page-level
// `columns` above does not reach it);
// - a group whose members are all empty renders nothing
// (`address` on a lead with no address);
// - members the highlights strip registered are dropped,
// so `assignment` (only `owner_id`) renders nothing on
// this page — the owner is in the strip;
// - a group declared `collapse: 'collapsed'` starts
// collapsed; changing that is a `fieldGroups` decision
// in lead.object.ts, not a key on this page.
sections: [
{
name: 'info',
label: 'Lead Information',
fields: ['salutation', 'first_name', 'last_name', 'title', 'company', 'industry'],
},
{
name: 'crm_contact',
label: 'Contact',
fields: ['email', 'phone', 'mobile', 'website'],
},
{
name: 'detail',
label: 'Lead Detail',
// `disqualification_reason` is mandatory on an
// Unqualified lead (see the validation on
// crm_lead) — the detail page has to show the
// recorded reason, not just the red status chip.
fields: ['status', 'disqualification_reason', 'rating', 'lead_source', 'owner_id', 'annual_revenue', 'number_of_employees'],
},
{
name: 'address',
label: 'Address',
fields: ['address'],
},
// The LINK half of the duplicate banner (#1207) —
// the banner says a record is repeated, this names it
// and lets the rep open it to compare.
//
// ⚠️ NOT the highlights strip, which is where this
// card's dispatch suggested it: `record:highlights`
// caps `fields` at 7 and the strip already carries 6,
// so the three duplicate fields would not fit without
// evicting a chip every lead needs to serve a state
// most leads are not in (measured: `objectstack
// validate` reports `fields: Too big: expected array
// to have <=7 items`, and
// `test/metadata-references.test.ts` parses the same
// props strictly, so it is a hard cap, not advice).
//
// A section costs nothing on a clean lead either, for
// a better reason than the strip's: `record:details`
// hides empty fields (`hideEmpty` defaults true in the
// renderer) and a section whose fields are ALL empty
// renders nothing — no heading, no empty shell (the
// measurement is in `test/detail-section-dedup.test.ts`).
// So this block appears exactly on the leads that carry
// a duplicate claim.
//
// All four fields, not just `duplicate_of_lead`:
// `lead_duplicate_check` matches CONTACTS first and
// only then open leads, so a suspected lead's survivor
// is a `crm_contact` at least as often as a `crm_lead`
// — naming only the lead link would leave the commoner
// half of the flagged population with a banner and
// nothing to click. `duplicate_status` also covers the
// state the banner deliberately does not: a `confirmed`
// verdict, and the `erased` tombstone that outlives the
// record it named.
{
name: 'duplicates',
label: 'Duplicate Management',
fields: [
'duplicate_status', 'duplicate_of_type',
'duplicate_of_lead', 'duplicate_of_contact',
],
},
{
name: 'description',
label: 'Description',
fields: ['description'],
columns: 1,
},
{ group: 'identity' },
{ group: 'company_info' },
{ group: 'contact_info' },
// Do Not Call / Email Opt Out, right under the numbers
// a rep is about to dial (#806).
{ group: 'preferences' },
{ group: 'qualification' },
{ group: 'assignment' },
{ group: 'address' },
{ group: 'conversion' },
// The LINK half of the duplicate banners (#1207): the
// group carries both survivor lookups, because
// `lead_duplicate_check` matches contacts before leads.
// On a clean lead every member is empty, so the section
// renders nothing. ⚠️ Not the highlights strip:
// `record:highlights` caps `fields` at 7 and holds 6.
{ group: 'duplicates' },
// Description and Notes — kept last, where the page's
// Description section always sat.
{ group: 'additional' },
],
},
},
Expand Down
Loading
Loading