Skip to main content

CnDetailPage

A generic detail/overview page component. The simpler counterpart to CnIndexPage — designed for pages that display statistics, charts, card grids, or other detail content without multi-object tables or CRUD dialogs.

Wraps: NcEmptyContent, NcLoadingIcon, NcButton (from @nextcloud/vue), CnIcon

Try it

Loading CnDetailPage playground…

Props

PropTypeDefaultDescription
titleString''Page title
descriptionString''Optional subtitle shown below the title
iconString''MDI icon name (rendered via CnIcon)
iconSizeNumber28Icon size in pixels
loadingBooleanfalseLoading state
loadingLabelString'Loading...'Message shown during loading
sidebarBoolean | ObjectfalseSidebar configuration. Accepts EITHER the legacy Boolean form (deprecated) OR the new Object form mirroring CnIndexPage.sidebar. See Sidebar config object below.
sidebarOpenBooleantrueWhether the sidebar starts open (only relevant when sidebar is active)
objectTypeString''Object type slug passed to the sidebar (e.g. 'pipelinq_lead')
objectIdString|Number''Object ID passed to the sidebar
sidebarPropsObject{}Extra sidebar configuration forwarded to CnObjectSidebar (register, schema, hiddenTabs, title, subtitle, tabs). Set sidebarProps.tabs to an open-enum tab array to drive the host app's mounted CnObjectSidebar from manifest.json — see CnObjectSidebar custom tabs. The array flows through the existing objectSidebarState provide/inject channel. Note: when both sidebar (Object) AND sidebarProps set the same field, the Object form wins and a console.warn lists the conflicting fields once per component instance.
errorBooleanfalseError state
errorMessageString'An error occurred'Message shown in error state
onRetryFunctionnullCallback for retry button in error state. If null, no retry button shown.
retryLabelString'Retry'Retry button text
emptyBooleanfalseEmpty state
emptyLabelString'No data available'Message shown in empty state
statsTitleString''Title above the statistics table
statsColumnsArray[]Column defs for stats table: [{ key: string, label: string, align?: 'left'|'center'|'right' }]
statsRowsArray[]Row data for stats table (objects keyed by column keys; set indent: true for sub-row styling)
maxWidthString'1200px'Maximum width of the page content

Slots

SlotScopeDescription
#iconCustom icon (replaces CnIcon)
#header-actionsAction buttons in the header (right side)
#errorCustom error state content
#error-actionsExtra buttons inside the default error state
#emptyCustom empty state content
#empty-actionsExtra buttons inside the default empty state
#stats-headerCustom header above the stats table (replaces default h3)
#stats-rowsCustom table body rows (replaces auto-generated rows)
#defaultMain content below the stats table
#sectionsAdditional content below the default slot
#footerFooter content (separated by a border)

CnDetailPage.sidebar accepts EITHER form:

  • Boolean (legacy, deprecated):sidebar="true" activates the external CnObjectSidebar via the objectSidebarState inject; false deactivates. The first time this form is observed per component instance a one-shot console.warn fires pointing at the migration path.

  • Object (preferred) — mirrors CnIndexPage.sidebar plus detail-specific fields:

    sidebar: {
    show: true, // default true; false suppresses the sidebar
    enabled: true, // default true; false bypasses the external sidebar
    register: 'leads', // forwarded via objectSidebarState
    schema: 'lead',
    hiddenTabs: ['notes'],
    title: 'Lead detail',
    subtitle: '...',
    tabs: [ // see manifest-abstract-sidebar
    { id: 'overview', label: 'lead.overview', widgets: [{ type: 'data' }] },
    ],
    }

    Use show: false to hide the sidebar declaratively without removing the rest of the config (e.g. behind a feature flag or a responsive layout watcher).

Migrating from boolean

Replace:

<CnDetailPage
:sidebar="true"
:sidebar-props="{ register: 'leads', schema: 'lead', tabs: [...] }"
object-type="lead"
:object-id="id" />

With:

<CnDetailPage
:sidebar="{ register: 'leads', schema: 'lead', tabs: [...] }"
object-type="lead"
:object-id="id" />

sidebarProps continues to work for backwards compatibility — when both sidebar (Object) and sidebarProps are set with overlapping fields, the Object form wins and a console.warn fires once per component instance listing the conflicting fields.

Usage

Basic detail page with statistics table

<template>
<CnDetailPage
title="Register Overview"
description="Statistics for this register"
icon="DatabaseOutline"
:loading="loading"
:stats-title="'Register Statistics'"
:stats-columns="[
{ key: 'type', label: 'Type' },
{ key: 'total', label: 'Total' },
{ key: 'size', label: 'Size' },
]"
:stats-rows="[
{ type: 'Objects', total: 150, size: '2.4 MB' },
{ type: 'Invalid', total: 3, size: '-', indent: true },
{ type: 'Deleted', total: 7, size: '-', indent: true },
{ type: 'Files', total: 42, size: '1.1 MB' },
{ type: 'Logs', total: 230, size: '512 KB' },
]">
<div class="chart-grid">
<ChartCard title="Audit Trail"><LineChart :data="auditData" /></ChartCard>
<ChartCard title="Objects by Schema"><PieChart :data="schemaData" /></ChartCard>
</div>
<div class="card-grid">
<SchemaCard v-for="schema in schemas" :key="schema.id" :schema="schema" />
</div>
</CnDetailPage>
</template>

With error handling and retry

<template>
<CnDetailPage
title="Schema Details"
:error="hasError"
error-message="Failed to load schema details"
:on-retry="loadSchema">
<template #error-actions>
<NcButton @click="$router.push('/registers')">
Back to Registers
</NcButton>
</template>
<DetailContent :schema="schema" />
</CnDetailPage>
</template>

Custom stats rows (manual table body)

When the auto-generated rows from statsRows aren't flexible enough, use the #stats-rows slot to render your own <tr> elements:

<template>
<CnDetailPage
title="Register Stats"
:stats-columns="[
{ key: 'type', label: 'Type' },
{ key: 'total', label: 'Total' },
{ key: 'size', label: 'Size' },
]">
<template #stats-rows>
<tr>
<td>Objects</td>
<td>{{ stats.objects?.total || 0 }}</td>
<td>{{ formatBytes(stats.objects?.size || 0) }}</td>
</tr>
<tr class="cn-detail-page__stats-row--sub">
<td class="cn-detail-page__stats-cell--indented">Invalid</td>
<td>{{ stats.objects?.invalid || 0 }}</td>
<td>-</td>
</tr>
</template>
</CnDetailPage>
</template>

When to use CnDetailPage vs other page components

ComponentUse when...
CnDetailPageDisplaying detail info, stats tables, charts, card overviews — no multi-object CRUD
CnIndexPageListing objects with table/cards, pagination, search, mass actions, CRUD dialogs
CnDashboardPageBuilding a widget-based dashboard with drag-and-drop grid layout

Collaborative editing defaults

CnDetailPage auto-subscribes to live updates for the current object when both objectStore and (objectType + objectId) are provided. This wires useObjectSubscription into the page lifecycle so users see remote changes without polling — including remote pessimistic locks.

When the cached @self.locked block indicates another user holds the lock, CnDetailPage mounts CnLockedBanner above the content. The banner renders only when lockedByMe === false.

Two opt-out props:

PropDefaultBehaviour
subscribetrueWhen false, skips the auto-subscribe (useful for read-only / archive views).
objectStorenullPinia store instance. When omitted, both subscribe and lock-state are skipped. Pass the result of useObjectStore() from your app.

See useObjectLock for the lock state contract; the lib does not yet auto-acquire on edit-mode toggle (planned for a follow-up cycle that wires the form dialogs).

Reference (auto-generated)

The tables below are generated from the SFC source via vue-docgen-cli. They reflect what's actually in CnDetailPage.vue and update automatically whenever the component changes.

Props

| Name | Type | Required | Default | Description | | -------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- | | layout | { id: number, widgetId: string, gridX: number, gridY: number, gridWidth: number, gridHeight?: number, showTitle?: boolean }[] | | [] | Grid layout definition. Array of placement objects defining where each widget appears in the 12-column grid. | | widgets | { id: string, title: string, type?: string }[] | | [] | Widget definitions. Array of widget objects with id and title. | | columns | number | | 12 | Number of grid columns. | | title | string | | '' | Page title | | description | string | | '' | Page description (shown below title) | | icon | string | | '' | Optional MDI icon name (rendered via CnIcon) | | iconSize | number | | 28 | Icon size in pixels | | loading | boolean | | false | Whether the page is in a loading state | | loadingLabel | string | | () =&gt; t('nextcloud-vue', 'Loading...') | Message shown during loading | | sidebar | union | | false | Sidebar configuration. Accepts EITHER form: - Boolean (legacy, deprecated): true activates the external sidebar, false deactivates. The first time this form is observed per component instance a one-shot console.warn is logged pointing at the migration path. Continues to work in v1.x for back-compat. - Object (preferred): mirrors CnIndexPage.sidebar plus the detail-specific fields previously on sidebarProps: ts { show?: boolean, // default true; false suppresses sidebar enabled?: boolean, // default true; false bypasses external sidebar register?: string, schema?: string, hiddenTabs?: string[], title?: string, subtitle?: string, tabs?: Array<TabDef>, // see manifest-abstract-sidebar } When BOTH sidebar (Object) and sidebarProps are set with overlapping fields, the Object form wins and a console.warn lists the conflicting fields once per component instance. | | sidebarOpen | boolean | | true | Whether the sidebar is open (expanded) | | objectType | string | | '' | The registered object type slug for the sidebar | | objectId | string | number | | '' | The object ID to display in the sidebar | | subtitle | string | | '' | Subtitle shown in the sidebar header | | sidebarProps | object | | \{\} | Additional sidebar configuration (register, schema, hiddenTabs, title, subtitle) | | error | boolean | | false | Whether the page is in an error state | | errorMessage | string | | () =&gt; t('nextcloud-vue', 'An error occurred') | Error message shown in error state | | onRetry | func | | null | Callback for retry button in error state. If null, no retry button is shown. | | retryLabel | string | | () =&gt; t('nextcloud-vue', 'Retry') | Label for the retry button | | empty | boolean | | false | Whether the page has no data to show | | emptyLabel | string | | () =&gt; t('nextcloud-vue', 'No data available') | Message shown when page is empty | | statsTitle | string | | '' | Title shown above the statistics table | | statsColumns | Array<{ key: string, label: string, align: string }> | | [] | Column definitions for the statistics table. Each column: { key: string, label: string, align?: 'left'\|'center'\|'right' } | | statsRows | Array<object> | | [] | Row data for the statistics table. Each row is an object keyed by column keys. Set indent: true on a row for sub-row styling. | | maxWidth | string | | '1200px' | Maximum width of the page content | | subscribe | boolean | | true | Whether to auto-subscribe to live updates for this object. Defaults to true. When useObjectStore and objectType + objectId are both available, the page calls objectStore.subscribe(objectType, objectId) on mount and unsubscribes on unmount via tryOnScopeDispose. Set false for read-only / archive views. | | objectStore | union | | null | Optional explicit Pinia store instance to subscribe / lock against. When omitted, the page resolves useObjectStore() lazily so consumer apps that haven't activated Pinia yet (e.g. tests) don't crash. |

Slots

NameBindingsDescription
headertitle, description, icon, icon-size
icon
actions
error
error-actions
empty
empty-actions
widget-${item.widgetId}name, item, widget
stats-header
stats-rows
default
sections
footer