Skip to main content

CnBodySections

Declarative in-body sections primitive — renders an array of section descriptors as titled cards in a page body, each hosting a registered host-app component with the object/page context injected.

This is the primitive that lets a rich bespoke detail page (BRP panels, activity timelines, comms history, relationship graphs, bookings, …) become declarative while keeping its sections in the page body. Unlike the pluggable integration registry — which forces every entry to declare BOTH a sidebar tab and a widget (parity contract) — a body section resolves its component straight from the v2 component registry (cnRegistry) or the legacy cnCustomComponents map, the same resolver CnPageRenderer uses for type:"custom" page components. No sidebar tab is required.

CnDetailPage mounts this automatically when the manifest page config declares bodyWidgets (one mount per placement slot). The component is reusable, so a dashboard / index page can adopt the same primitive later by passing its own context.

Registering a host component for in-body use

Register the component in the app's v2 registry under any kind that exposes a .component. The dedicated kind: "section" carries no grid metadata and no sidebar-tab parity requirement (validated by CnAppRoot):

// src/registry.js
import BrpContactPanel from './views/clients/BrpContactPanel.vue'

const registry = {
BrpContactPanel: { kind: 'section', component: BrpContactPanel },
// a kind:"widget" entry also resolves (it already exposes `.component`)
}

The host component reads the object either via token-resolved props or by injecting cnSectionContext:

// inject style — no manifest props needed
import { inject } from 'vue'
export default {
setup() {
const ctx = inject('cnSectionContext') // { objectId, object, register, schema } (a ref)
return { ctx }
},
}

Manifest config (consumed by CnDetailPage)

"config": {
"register": "pipelinq", "schema": "client",
"bodyWidgets": [
{
"id": "brp",
"component": "BrpContactPanel",
"title": "BRP",
"props": { "bsn": "@object.bsn", "objectId": "@objectId" },
"placement": "after-related",
"colSpan": 12
},
{
"id": "timeline",
"component": "ActivityTimeline",
"title": "Activity",
"props": { "entityId": "@objectId", "entityType": "client" }
}
]
}

Token resolution

Each props value is passed through resolveFilterValue with the page context:

TokenResolves to
@objectIdthe loaded object's id
@object.<field>a field off the loaded object
@workspace.<key>a value off the page/workspace context
@me, @now, @today±Nd, @monthStart, …the time/user tokens (see resolveFilterTokens)

A value that stays an unresolved @-token (an unset optional @workspace.<key>?, or an @object.<field> whose field is absent) is dropped from the props so the child receives undefined rather than a literal token string.

Placement

placement controls where in the detail body the section lands:

PlacementPosition
before-bodyabove the grid / stats / auto-body
after-databelow the data/auto-body, above the related-object lists
after-relatedbelow the related-object lists
end (default — also when placement is omitted)last body content, above the footer

Props

PropTypeDefaultDescription
sectionsArray[]Section descriptors: { id?, component, title?, props?, placement?, colSpan? }. component is a registry name resolved from cnRegistry then cnCustomComponents.
contextObject{}Page/object context ({ objectId?, object?, workspace?, register?, schema? }) used to resolve props tokens and provided on cnSectionContext for inject-based host components.
placementString | nullnullWhen set, only sections whose placement matches render (CnDetailPage mounts one instance per placement). null renders all passed sections.
gridBooleantrueLay sections out on a 12-column responsive grid honouring each section's colSpan. Falls back to a vertical stack when no section sets a colSpan.
errorLabelString'This section failed to load.'Inline message rendered when a section's component throws while rendering.

Error handling

  • A section whose component name is not registered renders an inline "not registered" message — the rest of the page is unaffected.
  • A section whose component throws while rendering is caught by a per-section error boundary (errorCaptured) and replaced with the errorLabel card; the error is logged once to the console and the sibling sections + page survive.

Provided context

cnSectionContext — a reactive ref holding { objectId, object, register, schema } for the current page, injectable by any descendant section component.