CnDetailWidgetHost
Renders one detail-page widget definition, with or without card chrome.
Why it exists
This was a 230-line v-if chain inside CnDetailPage's grid slot. It moved out when a second surface needed it: CnTabsWidget renders the same widget definitions inside tab panels, without the cards.
Two copies of "which renderer does this type get" is the kind of thing that drifts. A type added to one and not the other renders correctly on the page and blank in a tab, with nothing in the console either way. That is how 101 of hrmq's 236 detail widgets went missing once, when the dashboard catalog and BUILT_IN_WIDGETS were two near-disjoint vocabularies.
The pure decisions live in utils/widgetDispatch.js, which both surfaces import. This component is the markup that acts on them.
The two chrome modes
chrome="card" is the detail-page grid: every widget gets a titled card, because ADR-062 rule 5 says every body widget carries chrome and its manifest title.
chrome="bare" is a tab panel. The strip already shows the title and draws the card, so a second title inside the panel reads as a heading nested in its own heading. Bare mode drops the wrapper and, for an integration leaf, renders the provider's tab (its bare content) rather than its widget (which draws its own card). That pairing is not new: CnIntegrationWidget already does exactly this inside its own panels.
What bare mode deliberately does not strip
The data widget keeps its chrome in both modes. CnWidgetWrapper renders its actions inside the header, and CnObjectDataWidget puts its Save button there, so suppressing the header to remove a duplicate title would also remove the only way to commit an inline edit. A silently unsaveable form is worse than a doubled title.
When the widget leans on another app
A widget definition may carry requiredApp — the id of another Nextcloud app it depends on, either because its data lives in that app's register or because its component comes from that app's integration leaf:
{
"id": "case-hours",
"type": "stats-block",
"title": "Hours booked",
"requiredApp": "humaniq",
"content": { "entries": [{ "register": "humaniq", "schema": "TimeEntry", "metric": "sum" }] }
}
When that app is absent the host renders the widget's normal chrome plus a set-up state, and issues no request.
It renders rather than hides on purpose. A hidden widget leaves a hole a reader cannot interpret; letting the query run is worse. dossiq's hours tile aggregated humaniq's register, and on every install without humaniq the request 404'd and the tile showed 0 — which is exactly what a real zero shows. It looked correct on every case in every such install.
requiredApp is read from the definition first and content second, so it can sit wherever the rest of that widget's config lives. It is declared rather than inferred from content.register, because a register slug is not always an app id and a widget can depend on an app without querying it.
Integration widgets (type: "integration") already degrade this way through the registry's own requiredApp; this extends the same behaviour to every other widget type.
Usage
<CnDetailWidgetHost
:widget="def"
chrome="bare"
:object-id="id"
:object="record"
register="dossiq"
schema="case" />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
widget | Object | required | The resolved widget definition: { id, type, title, icon, content }, plus integrationId for type: 'integration'. |
chrome | String | 'card' | 'card' for a titled CnWidgetWrapper, 'bare' when the caller owns the title and the card. |
objectId | String | Number | '' | The bound record's id. Present on the first render, unlike object. |
object | Object | null | The loaded record, or null while it is still being fetched. |
objectType | String | '' | The resolved object-type slug. |
schemaObject | Object | null | The resolved JSON Schema, needed by the data widget. |
register | String | Object | '' | OpenRegister register slug of the surface. |
schema | String | Object | '' | OpenRegister schema slug of the surface. |
store | Object | null | The effective object store. |
surface | String | 'detail-page' | Rendering surface forwarded to integration widgets (AD-19). |
integrationContext | Object | null | Object context forwarded to integration widgets. The surface derives this: see the note below. |
hideEmpty | Boolean | false | Hide empty properties in the data widget. |
availableWidgets | Array | [] | Every widget definition on the surface. Only a container type receives it; a leaf widget has no business knowing what else is on the page. |
showCardTitle | Boolean | null | Whether a card widget draws the wrapper header. Null means decide from the definition. |
cnRegistry | Object | {} | The consumer's component registry, consulted before the built-in catalog so a custom type overrides a built-in. |
Events
| Event | Payload | Description |
|---|---|---|
geo-saved | object | The geo widget saved a geometry. |
open-integration | string | The related widget asked to open an integration. |
Notes
integrationContextis derived by the surface, not here.CnDetailPagefalls back through its resolved sidebar config andsidebarPropsbefore its own register/schema. Deriving fromregister/schemaalone hands the widget two empty strings.showCardTitleis a prop, not a computed. The rule reads the layout item'sshowTitle, and only the surface holds the layout. Deriving it from the widget definition alone silently dropsshowTitle: false, which is the one way a consumer can stop a card printing its title twice.- The
cn-detail-page__card-fitandcn-detail-page__catalog-cardclass names kept their old prefix although the markup moved here.src/css/detail-page.csstargets them and so may consumer CSS. - An unresolvable type renders nothing, which is what
CnDetailPagehas always done at this point. Surfacing a placeholder would be more honest, but this is the slot fallback for every detail page in the fleet, so an app that declares a custom widget and omits its#widget-<id>slot would start showing a box where it shows nothing today.
See also
CnDetailPage, the card-mode surfaceCnTabsWidget, the bare-mode surface
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
widget | object | ✓ | — | The resolved widget definition: { id, type, title, icon, content }, plus integrationId for type: 'integration'. Resolved, not a layout item: turning an item into a definition needs the surface's own widget list, which is the one part the two surfaces genuinely do differently. |
chrome | string | 'card' | How much chrome to draw around the widget. - 'card' — a titled CnWidgetWrapper, the detail-page grid default. - 'bare' — no wrapper; the caller owns the title and the card. | |
objectId | string|number | '' | The bound record's id. Present on the first render; object is not. | |
object | object | null | The loaded record, or null while it is still being fetched. | |
objectType | string | '' | The resolved object-type slug. | |
schemaObject | object | null | The resolved JSON Schema object, needed by the data widget. | |
register | string|object | '' | OpenRegister register slug of the surface. | |
schema | string|object | '' | OpenRegister schema slug of the surface. | |
store | object | null | The effective object store. | |
surface | string | 'detail-page' | Rendering surface forwarded to integration widgets (AD-19). | |
integrationContext | object | null | Object context forwarded to integration widgets. | |
hideEmpty | boolean | false | Hide empty properties in the data widget. | |
availableWidgets | object[] | [] | Every widget definition on the surface, for a CONTAINER widget to resolve the children it references by id. Only container types receive it (see rendererProps). A leaf widget has no business knowing what else is on the page. | |
showCardTitle | union | null | Whether a card widget (stat / gauge / delta) draws the wrapper header. A prop, not a computed, because the rule reads the LAYOUT item's showTitle and only the surface holds the layout. Deriving it from the widget definition alone silently drops showTitle: false, which is the one way a consumer can stop a card printing its title twice: the tile already draws content.label itself. Null means "decide from the definition", which is what a surface with no layout (a tab panel) wants. | |
cnRegistry | object | \{\} | The consumer's component registry, consulted before the built-in catalog so a custom widget type overrides a built-in (REQ-MVR-005). |
Events
| Name | Payload | Description |
|---|---|---|
geo-saved | — | Emitted when the geo widget saved a geometry. |
open-integration | — | Emitted when the related widget asks to open an integration. |