CnDashboardPage
Top-level dashboard page component — the dashboard equivalent of CnIndexPage. Assembles a complete dashboard from a widgets definition array and a layout array. Supports custom widgets via scoped slots, Nextcloud Dashboard API widgets, tile widgets, and an optional drag-and-drop edit mode.
Wraps: CnDashboardGrid, CnWidgetWrapper, CnWidgetRenderer, CnTileWidget
Try it
Widget types
| Type | How to use |
|---|---|
| Tile | Widget def has type: 'tile' — renders as a quick-access link tile |
| Custom | Provide a #widget-{widgetId} scoped slot (escape hatch — beats every built-in branch when a slot exists) |
| Chart | Widget def has type: 'chart' — declarative apexcharts mount via CnChartWidget; chart inputs ride widgetDef.props |
| NC Dashboard API | Widget def has itemApiVersions — auto-rendered via CnWidgetRenderer |
The dispatcher resolves widgets in that order. The custom-slot branch beats the chart branch so apps that need bespoke apexcharts behaviour outside the manifest contract can fall back to #widget-{id} without losing the rest of the manifest.
Chart widget
const WIDGETS = [{
id: 'sla-trend',
title: 'SLA trend',
type: 'chart',
props: {
chartKind: 'line', // line | bar | donut | area | pie | radialBar
series: [{ name: 'SLA %', data: [82, 88, 91, 93] }],
categories: ['Q1', 'Q2', 'Q3', 'Q4'],
options: { stroke: { width: 3 } }, // deep-merged with CnChartWidget defaults
height: 280,
// Reserved for a future cycle — not read at render time today:
// dataSource: { url: '/index.php/apps/myapp/api/charts/sla' }
// dataSource: { register: 'cases', schema: 'case', groupBy: 'caseType', aggregate: 'count' }
},
}]
Forwarded props keys (everything else is ignored): chartKind (→ type), series, categories, labels, options, colors, toolbar, legend, height, width, unavailableLabel.
Usage
<template>
<CnDashboardPage
title="Dashboard"
:widgets="WIDGETS"
:layout="layout"
:loading="loading"
:allow-edit="true"
@layout-change="saveLayout"
@edit-toggle="onEditToggle">
<!-- Custom widgets -->
<template #widget-kpis="{ item }">
<CnKpiGrid :items="kpiData" />
</template>
<template #widget-cases-chart="{ item }">
<CnChartWidget type="pie" :series="chartSeries" :labels="chartLabels" />
</template>
<!-- Per-widget header actions -->
<template #widget-kpis-actions="{ item }">
<NcButton type="tertiary" @click="refreshKpis">Refresh</NcButton>
</template>
<template #header-actions>
<NcButton @click="addWidget">Add widget</NcButton>
</template>
</CnDashboardPage>
</template>
<script>
const WIDGETS = [
{ id: 'kpis', title: 'Key Metrics', type: 'custom' },
{ id: 'cases-chart', title: 'Cases by status', type: 'custom', iconClass: 'icon-chart' },
]
const DEFAULT_LAYOUT = [
{ id: 1, widgetId: 'kpis', gridX: 0, gridY: 0, gridWidth: 12, gridHeight: 2, showTitle: false },
{ id: 2, widgetId: 'cases-chart', gridX: 0, gridY: 2, gridWidth: 6, gridHeight: 4 },
]
</script>
Use the useDashboardView composable to manage widget state and layout persistence:
import { useDashboardView } from '@conduction/nextcloud-vue'
const { widgets, layout, loading, onLayoutChange } = useDashboardView({
widgets: WIDGETS,
defaultLayout: DEFAULT_LAYOUT,
loadLayout: () => loadFromConfig('dashboard_layout'),
saveLayout: (l) => saveToConfig('dashboard_layout', l),
})
Props
| Prop | Type | Default | Description |
|---|---|---|---|
title | String | '' | Page title |
description | String | '' | Description shown below the title |
widgets | Array | [] | Widget definition objects (see Widget definition below) |
layout | Array | [] | Grid placement objects (see Layout item below) |
loading | Boolean | false | Show loading spinner instead of the grid |
allowEdit | Boolean | false | Show the Edit/Done toggle button |
columns | Number | 12 | Number of grid columns |
cellHeight | Number | 80 | Grid cell height in pixels |
gridMargin | Number | 12 | Gap between grid items in pixels |
editLabel | String | 'Edit' | Label for the edit button |
doneLabel | String | 'Done' | Label for the done button |
emptyLabel | String | 'No widgets configured' | Empty state message |
unavailableLabel | String | 'Widget not available' | Fallback for unknown widget IDs |
dateRange | Object | null | Optional date-range header descriptor — see Date-range header |
Widget definition
| Field | Type | Description |
|---|---|---|
id | String | Unique widget identifier |
title | String | Widget title shown in the wrapper header |
type | String | 'custom' (default), 'tile', or 'chart'. 'chart' mounts CnChartWidget; chart inputs ride props |
props | Object | Free-form widget-specific props. For chart widgets: { chartKind, series, categories, labels, options, colors, toolbar, legend, height, width, unavailableLabel, dataSource? } |
iconUrl | String | Header icon image URL |
iconClass | String | Header icon CSS class |
titleIconPosition | String | Position of the widget-{id}-title-icon slot: 'left' (before title) or 'right' (after actions, default) |
titleIconColor | String | CSS color applied to the title-icon slot container (e.g. '#e74c3c') |
buttons | Array | Footer buttons: [{ text, link }] |
itemApiVersions | Number[] | NC Dashboard API versions — triggers auto-rendering |
reloadInterval | Number | Auto-refresh interval in seconds (NC widgets) |
Layout item
| Field | Type | Description |
|---|---|---|
id | String | Number | Unique placement ID |
widgetId | String | References a widget id from the widgets array |
gridX | Number | Column start (0-based) |
gridY | Number | Row start (0-based) |
gridWidth | Number | Width in grid columns |
gridHeight | Number | Height in grid rows |
showTitle | Boolean | Whether to show the wrapper header (default true) |
styleConfig | Object | Style overrides passed to CnWidgetWrapper |
Events
| Event | Payload | Description |
|---|---|---|
layout-change | layout[] | Emitted when the user drags or resizes a widget; payload is the full updated layout array |
edit-toggle | boolean | Emitted when the Edit/Done button is clicked; payload is the new editing state |
date-range-change | { from, to, preset } | Emitted on every range change AND on mount when the date-range feature is enabled. Tracks the picker's current value. |
Slots
| Slot | Scope | Description |
|---|---|---|
header-actions | — | Extra buttons in the page header (right side) |
widget-{widgetId} | { item, widget } | Custom content for a specific widget |
widget-{widgetId}-actions | { item, widget } | Header action buttons for a specific widget |
widget-{widgetId}-title-icon | { item, widget } | Extra icon in the widget header; position and color controlled by titleIconPosition / titleIconColor on the widget definition |
empty | — | Custom empty state when no layout items exist |
Date-range header
When the dateRange prop is set with enabled: true, the dashboard renders a CnDateRangePicker between the page header and the widget grid. The selected range is:
- emitted on every change via
@date-range-change, - optionally persisted to
localStorage(whenpersistKeyis set), - provided to every descendant widget through the
cnDashboardDateRangeinjection key as a reactive Vue ref.
<CnDashboardPage
title="Dashboard"
:widgets="WIDGETS"
:layout="layout"
:date-range="{
enabled: true,
persistKey: 'myapp.dashboard.range',
default: { preset: 'last-7' },
}"
@date-range-change="onRangeChange" />
dateRange shape
| Field | Type | Description |
|---|---|---|
enabled | Boolean | When true, renders the picker row. When false / omitted, no row appears. |
default | Object (opt.) | Initial { from, to, preset? } when no persisted state is found. |
persistKey | String (opt.) | When set, the chosen range is persisted to localStorage[persistKey]. |
presets | Array (opt.) | Override the preset list. See DEFAULT_DATE_RANGE_PRESETS. |
The resolution order is: explicit default → rehydrated localStorage (when persistKey set) → last-7 preset (now − 7d → now).
cnDashboardDateRange provide / inject
CnDashboardPage always provides cnDashboardDateRange — even when the feature is off — so descendants can inject without a fallback dance:
import { inject, ref } from 'vue'
export default {
setup() {
const range = inject('cnDashboardDateRange', ref(null))
// range.value is `{ from, to, preset }` when the feature is on,
// and `null` when it's off.
return { range }
},
}
CnChartWidget consumes this injection automatically — see its bucket data-source documentation.
Built-in page-level Actions menu
The dashboard header carries the shared CnActionsMenu overflow … — Refresh, Documentation, and Request a feature — next to the edit toggle. This is the page-level menu, distinct from each widget's own menu (the per-widget ones emit @widget-refresh / @widget-request-feature).
- Refresh emits
@refreshand, unless suppressed viaevent.preventDefault(), fires thecn:page:refreshevent-bus channel with{ widgetId, title }. - Documentation renders only when
documentationUrlis set, opening it in a new tab. - Request a feature opens
CnSuggestFeatureModalwithsurface: "dashboard:<id>"when mounted underCnAppRoot.
Refresh and Request-a-feature are on by default; opt out with :show-refresh="false" / :show-request-feature="false". Set :page-id for a stable id/surface.
Reference (auto-generated)
The tables below are generated from the SFC source via vue-docgen-cli. They reflect what's actually in CnDashboardPage.vue and update automatically whenever the component changes.
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | '' | Page title | |
description | string | '' | Page description (shown below title) | |
widgets | Array<{ id: string, title: string, type: string, iconUrl: string, iconClass: string, buttons: Array, itemApiVersions: number[], reloadInterval: number, props: object }> | [] | Widget definitions array. Each widget defines metadata for rendering. Custom widgets: { id: 'my-widget', title: 'My Widget', type: 'custom' } NC API widgets: { id: 'calendar', title: 'Calendar', itemApiVersions: [1,2], ... } Tile widgets: { id: 'tile-files', type: 'tile', title: 'Files', icon: 'M12...', iconType: 'svg', backgroundColor: '#0082c9', textColor: '#fff', linkType: 'app', linkValue: 'files' } Chart widgets: { id: 'sla', type: 'chart', title: 'SLA trend', props: { chartKind: 'line', series: [{ name: 'SLA %', data: [82, 88, 91] }], categories: ['Q1', 'Q2', 'Q3'], options: { stroke: { width: 3 } } } } | |
layout | union | [] | Layout array defining widget positions in the grid. Each item: { id: 'unique-id', widgetId: 'my-widget', gridX: 0, gridY: 0, gridWidth: 4, gridHeight: 3 } Additional properties (showTitle, styleConfig, tile config) are passed through. | |
content | Array<{ type: string, ref: string }> | [] | Declarative content items. Each item is a widget-ref entry from the manifest's pages[].config.content[] array: { type: 'widget-ref', ref: 'openregister://widget/<schemaSlug>/<widgetSlug>' } CnDashboardPage renders each widget-ref item as a CnWidgetRefItem which resolves the widget from OR's registry at runtime and renders the resolved component. Only widget-ref entries are processed; unknown type values are skipped with a console.warn. When both content (widget-ref items) and widgets+layout (classic GridStack layout) are present, content items are rendered above the grid in a stacked list. | |
loading | boolean | false | Whether the dashboard is loading | |
allowEdit | boolean | false | Whether to show the edit toggle button | |
columns | number | 12 | Number of grid columns | |
cellHeight | number | 80 | Grid cell height in pixels | |
gridMargin | number | 12 | Grid margin in pixels | |
editLabel | string | () => t('nextcloud-vue', 'Edit') | Label for the edit button | |
doneLabel | string | () => t('nextcloud-vue', 'Done') | Label for the done button (when editing) | |
emptyLabel | string | () => t('nextcloud-vue', 'No widgets configured') | Label for the empty state | |
unavailableLabel | string | () => t('nextcloud-vue', 'Widget not available') | Label for unavailable widgets | |
surface | string | 'app-dashboard' | Rendering surface forwarded to integration widgets (widgets whose type === 'integration'). Drives the AD-19 surface fallback on resolveWidget(integrationId, surface). | |
integrationContext | union | null | Object context forwarded to integration widgets: { register, schema, objectId }. Optional — most dashboards aren't object-scoped, but CnDetailPage passes one through so CnFilesCard / CnTagsCard / CnAuditTrailCard know which object's sub-resources to fetch. | |
dateRange | union | null | Optional date-range header descriptor. When enabled: true the dashboard renders a CnDateRangePicker between the header and the widget grid, persists the chosen range to localStorage (when persistKey is set), emits @date-range-change on every change, AND provides a reactive cnDashboardDateRange ref to every descendant widget. When the prop is null (default), false, or { enabled: false }, the header row is NOT rendered and the existing dashboard layout stays unchanged. The provide is still installed (always) but its value stays null so descendant chart widgets fall back to their dataSource.bucket.staticRange (or skip the query entirely). Shape: ts { enabled: boolean, default?: { from: string, to: string, preset?: string }, persistKey?: string, presets?: Array<{ id: string, label: string, days: number|null }>, } Default preset when no explicit default and no persisted state is found: last-7 (now − 7d → now). | |
showRefresh | boolean | true | Show the built-in Refresh item in the page-level overflow Actions menu (distinct from the per-widget menus). On by default. The default handler emits @refresh and, unless suppressed, fires the cn:page:refresh event-bus channel. | |
showRequestFeature | boolean | true | Show the built-in Request-a-feature item in the page-level overflow Actions menu. On by default; opens the CnSuggestFeatureModal when mounted under CnAppRoot. | |
documentationUrl | string | '' | Documentation link for this dashboard. When a non-empty URL is set, the page-level overflow menu renders a "Documentation" item that opens the link in a new tab. Empty (the default) hides it. | |
documentationLabel | string | () => t('nextcloud-vue', 'Documentation') | Pre-translated label for the Documentation action. Defaults to "Documentation". | |
pageId | string | '' | Stable id for this dashboard, used in the @refresh / @request-feature payloads and the surface: "dashboard:<id>" field on the feature-request modal. Falls back to a slugified title when unset. | |
specRef | string | '' | Optional specRef forwarded to the feature-request modal. | |
refreshing | boolean | false | Whether a page-level refresh is in flight (drives the Refresh icon spin). | |
optimisticSpinMs | number | 800 | Optimistic Refresh-icon spin duration (ms) when refreshing is unbound. | |
refreshLabel | string | () => t('nextcloud-vue', 'Refresh') | Pre-translated label for the Refresh action. | |
requestFeatureLabel | string | () => t('nextcloud-vue', 'Request a feature') | Pre-translated label for the Request-a-feature action. | |
actionsMenuLabel | string | () => t('nextcloud-vue', 'Actions') | Pre-translated aria-label / tooltip for the overflow menu trigger. |
Events
| Name | Payload | Description |
|---|---|---|
layout-change | — | Emitted when the user finishes dragging/resizing a widget. Payload: the updated layout array [{ widgetId, x, y, w, h }, ...]. |
edit-toggle | — | Emitted when the user toggles edit mode. Payload: true when entering edit mode, false when leaving. |
date-range-change | — | Fired whenever the dashboard's effective date range changes (initial resolve, picker change, or persisted-range restore). Payload: { from, to, preset }. |
refresh | undefined | User clicked Refresh in the page-level overflow Actions menu. Payload: { widgetId, title }. Handlers may call the second arg's preventDefault() to suppress the built-in default (event-bus emit on cn:page:refresh). |
request-feature | undefined | User clicked Request a feature in the page-level overflow Actions menu. Payload: { widgetId, title }. Handlers may call the second arg's preventDefault() to suppress the built-in default (auto-opening CnSuggestFeatureModal). |
widget-refresh | — | User clicked Refresh in a widget's overflow action menu. Payload: the layout item descriptor. |
widget-request-feature | — | User clicked Request a feature in a widget's overflow action menu. Payload: the layout item descriptor. |
Slots
| Name | Bindings | Description |
|---|---|---|
header-actions | — | header-actions Inline buttons rendered in the dashboard header next to the edit toggle. Used by every existing consumer (decidesk, mydash, opencatalogi, pipelinq, procest). |
actions | — | actions Back-compat alias for #header-actions. Prefer #header-actions in new code. |
action-items | — | |
empty | — | empty Replaces the default empty state shown when the dashboard has no widgets. Defaults to an NcEmptyContent block. |
'widget-' + item.widgetId + '-title-icon' | name, item, widget | |
'widget-' + item.widgetId + '-actions' | name, item, widget | |
'widget-' + item.widgetId | name, item, widget | widget-{widgetId} Per-widget body content (e.g. #widget-my-work). Apps inject custom widget rendering here. Scope: { item, widget }. |