Ga naar hoofdinhoud

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

Loading CnDashboardPage playground…

Widget types

TypeHow to use
TileWidget def has type: 'tile' — renders as a quick-access link tile
CustomProvide a #widget-{widgetId} scoped slot (escape hatch — beats every built-in branch when a slot exists)
ChartWidget def has type: 'chart' — declarative apexcharts mount via CnChartWidget; chart inputs ride widgetDef.props
NC Dashboard APIWidget 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

PropTypeDefaultDescription
titleString''Page title
descriptionString''Description shown below the title
widgetsArray[]Widget definition objects (see Widget definition below)
layoutArray[]Grid placement objects (see Layout item below)
loadingBooleanfalseShow loading spinner instead of the grid
allowEditBooleanfalseShow the Edit/Done toggle button
columnsNumber12Number of grid columns
cellHeightNumber80Grid cell height in pixels
gridMarginNumber12Gap between grid items in pixels
editLabelString'Edit'Label for the edit button
doneLabelString'Done'Label for the done button
emptyLabelString'No widgets configured'Empty state message
unavailableLabelString'Widget not available'Fallback for unknown widget IDs
dateRangeObjectnullOptional date-range header descriptor — see Date-range header
pageFiltersArray[]Optional page-level filter controls rendered in the header — see Page filters. Each entry \{ key, label?, type?: 'select', options: [\{ value, label \}], default? \}. The selection is written into the reactive workspace context, so widgets can read it via a @page.<key> / @workspace.<key> token.

Widget definition

FieldTypeDescription
idStringUnique widget identifier
titleStringWidget title shown in the wrapper header
typeString'custom' (default), 'tile', or 'chart'. 'chart' mounts CnChartWidget; chart inputs ride props
propsObjectFree-form widget-specific props. For chart widgets: { chartKind, series, categories, labels, options, colors, toolbar, legend, height, width, unavailableLabel, dataSource? }
iconUrlStringHeader icon image URL
iconClassStringHeader icon CSS class
titleIconPositionStringPosition of the widget-{id}-title-icon slot: 'left' (before title) or 'right' (after actions, default)
titleIconColorStringCSS color applied to the title-icon slot container (e.g. '#e74c3c')
buttonsArrayFooter buttons: [{ text, link }]
itemApiVersionsNumber[]NC Dashboard API versions — triggers auto-rendering
reloadIntervalNumberAuto-refresh interval in seconds (NC widgets)

Layout item

FieldTypeDescription
idString | NumberUnique placement ID
widgetIdStringReferences a widget id from the widgets array
gridXNumberColumn start (0-based)
gridYNumberRow start (0-based)
gridWidthNumberWidth in grid columns
gridHeightNumberHeight in grid rows
showTitleBooleanWhether to show the wrapper header (default true)
flushBooleanWhether the widget body renders flush — edge-to-edge with no wrapper content padding. Default true for dashboard content widgets (tables, KPIs, charts, custom/integration), so a borderless table or KPI meets the card edges. Set flush: false to restore the padded box for content that needs breathing room.
dateChipBooleanShow the per-widget date-range chip (see Per-card date chip)
showActionsBooleanForwarded to CnWidgetWrapper — set false to drop the header overflow menu on compact tiles
styleConfigObjectStyle overrides passed to CnWidgetWrapper

Events

EventPayloadDescription
layout-changelayout[]Emitted when the user drags or resizes a widget; payload is the full updated layout array
edit-togglebooleanEmitted 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.
page-filter-change{ key, value }Emitted when a page filter selection changes. The value is also written into the reactive workspace context.

Slots

SlotScopeDescription
header-actionsExtra 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
emptyCustom 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 (when persistKey is set),
  • provided to every descendant widget through the cnDashboardDateRange injection key as a reactive Vue ref,
  • published into the page-level workspace context as dateFrom / dateTo / datePreset, so any declarative widget can scope itself to the active window via the optional filter tokens @workspace.dateFrom? / @workspace.dateTo? (see Re-scoping KPI counts).
<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

FieldTypeDescription
enabledBooleanWhen true, renders the picker row. When false / omitted, no row appears.
controlString (opt.)Header control style: 'picker' (default — CnDateRangePicker: a preset select + two date inputs) or 'pills' (a compact segmented toggle-button row).
defaultObject (opt.)Initial { from, to, preset? } when no persisted state is found.
persistKeyString (opt.)When set, the chosen range is persisted to localStorage[persistKey].
presetsArray (opt.)Override the preset list. See DEFAULT_DATE_RANGE_PRESETS. A preset with no numeric days / hours (other than the manual custom entry) — e.g. { id: 'all', label: 'All', days: null } — or one with clear: true is an "All" / clear option that removes the window.

The resolution order is: explicit default → rehydrated localStorage (when persistKey set) → last-7 preset (now − 7d → now).

Re-scoping KPI counts by the active window

The chosen range is published into the page-level workspace context as dateFrom / dateTo / datePreset. A declarative widget (e.g. a stat KPI tile) scopes its query to the window by referencing the OPTIONAL tokens in its source.filter on whichever date field it tracks:

{
"type": "stat",
"content": {
"label": "Decisions",
"route": { "name": "Decisions" },
"source": {
"register": "decidesk", "schema": "decision", "metric": "count",
"filter": {
"decisionDate": { "gte": "@workspace.dateFrom?", "lte": "@workspace.dateTo?" }
}
}
}
}

The trailing ? marks each token OPTIONAL: when the "All" preset clears the window (empty bounds) the token stays unresolved and is dropped, so the date filter is omitted and the tile shows its unfiltered count.

Per-card date chip (layout[].dateChip)

Instead of (or alongside) the page-level header control, a KPI card can carry its own period selector. Set dateChip: true on the widget's layout entry. When the card shows its header (showTitle not false) the chip sits in the header next to the title; on a flush card (showTitle: false) it floats in the top-right corner over the content — so a compact KPI tile keeps its clean look and still gets a date selector. This works for the registered card widgets (stat / gauge / delta) and any custom-slot widget. The chip opens the same preset popover and drives the same shared dashboard range, so every card stays in sync. Its label shows the active preset (e.g. 7d / 30d / All) and it stays clickable even on the unbounded "All" range. Pair with dateRange.showHeaderPicker: false to drop the page-level control and rely solely on the per-card chips:

{
"config": {
"dateRange": {
"enabled": true,
"showHeaderPicker": false,
"default": { "preset": "all" },
"presets": [
{ "id": "last-7", "label": "7d", "days": 7 },
{ "id": "last-30", "label": "30d", "days": 30 },
{ "id": "all", "label": "All", "days": null }
]
},
"layout": [
{ "id": "1", "widgetId": "decisions-kpi", "gridX": 0, "gridY": 0, "gridWidth": 3, "gridHeight": 2, "showTitle": true, "dateChip": true }
]
}
}

A preset with no numeric days / hours (other than custom) — e.g. { id: "all", days: null } — or one flagged clear: true clears the window.

Pills control (control: 'pills')

control: 'pills' replaces the bulky select + two date inputs with a compact segmented toggle row — one pill per preset, rendered as role="group" with aria-pressed on the active pill (WCAG-friendly toggle group). A de-emphasised "Custom range" pill (dashed outline) opens a small from/to popover, so the arbitrary-window affordance stays without two bare date inputs sitting in the header. The active pill drives the same shared dashboard range as the default picker — no other wiring changes.

<CnDashboardPage
:date-range="{
enabled: true,
control: 'pills',
default: { preset: 'last-30' },
presets: [
{ id: 'last-7', label: 'Last 7 days', days: 7 },
{ id: 'last-30', label: 'Last 30 days', days: 30 },
{ id: 'last-90', label: 'Last 90 days', days: 90 },
{ id: 'custom', label: 'Custom range', days: null },
],
}" />

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.

Page filters

The pageFilters prop renders page-level select controls in the dashboard header. Each selection is written into the reactive workspace context (the same cnWorkspaceContext provide widgets read), so any widget can react to it through a @page.<key> / @workspace.<key> token. This is how a single period selector drives every endpoint-bound KPI on the page.

<CnDashboardPage
title="Analytics"
:widgets="WIDGETS"
:layout="layout"
:page-filters="[
{
key: 'period',
label: 'Period',
type: 'select',
default: 'last-30',
options: [
{ value: 'last-7', label: 'Last 7 days' },
{ value: 'last-30', label: 'Last 30 days' },
],
},
]"
@page-filter-change="onPeriodChange" />

A widget then binds its data to the selected value. With an endpoint-bound stat widget:

{
"id": "leads", "type": "stat",
"content": {
"label": "Leads",
"source": { "kind": "endpoint", "url": "/api/analytics/summary", "path": "totalLeads", "params": { "period": "@page.period" } }
}
}

Each entry: key (the context key written on change), optional label, type (only 'select' today), options ([{ value, label }]), and an optional default (else the first option seeds the context on mount). Omitting pageFilters renders nothing and leaves the context untouched (backwards-compatible).

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 @refresh and, unless suppressed via event.preventDefault(), fires the cn:page:refresh event-bus channel with { widgetId, title }.
  • Documentation renders only when documentationUrl is set, opening it in a new tab.
  • Request a feature opens CnSuggestFeatureModal with surface: "dashboard:<id>" when mounted under CnAppRoot.

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.

Each per-widget menu also surfaces a Documentation item. Its URL resolves to the widget def's own documentationUrl when set, otherwise it inherits the page-level documentationUrl — so a documented dashboard shows Documentation on every widget without per-widget config.

In-body sections (bodyWidgets)

The bodyWidgets prop hosts registered host-app section components alongside the widget grid — the dashboard equivalent of CnDetailPage's bodyWidgets. This is how a bespoke analytics dashboard (a custom funnel / time-series / channel-bar chart that reads its own REST endpoint) becomes a type: "dashboard" page without rewriting the charts — the chart stays a registered component and surfaces as an in-body section.

Each section resolves its component from the v2 cnRegistry (any entry exposing a .component) first, then the legacy cnCustomComponents map — the same resolver type: "custom" pages use. No sidebar tab is required (unlike integration widgets). A name in neither registry renders an inline error; the page never breaks. Reuses CnBodySections.

{
"type": "dashboard",
"config": {
"bodyWidgets": [
{ "id": "funnel", "component": "ConversionFunnel", "title": "Funnel",
"placement": "before-grid",
"props": { "period": "@workspace.period", "currency": "@config.currency" } },
{ "id": "channels", "component": "ChannelBarChart", "placement": "after-grid", "colSpan": 6 }
]
}
}
  • placementbefore-grid (above the grid, renders even when there are no grid widgets), after-grid (below it), or end (the default; the last body content). Each section renders exactly once.
  • props — token-resolved against the page context: @workspace.<key> / @page.<key> (page-level workspace state, e.g. a pageFilters selection), @config.<key> (app config, see below), plus the time / @me / @objectId tokens. An unresolved optional token (@workspace.<key>?) is dropped so the child sees undefined.
  • colSpan — 1–12 grid span when sections at one placement share a row.

The section context ({ register, schema, objectId, workspace, config }) is also provided on cnSectionContext, so a section component can inject('cnSectionContext') instead of taking explicit props. The optional object scoping (register / schema / objectId) comes from integrationContext.

App config tokens (@config.<key>)

The appConfig prop is a page-level config map exposed to declarative widget / section config via the @config.<key> token, and provided to descendants on cnAppConfig. Its primary use is making a setup-wizard-captured value (e.g. the reporting currency) actually format the dashboards instead of a hard-coded EUR:

{
"id": "revenue", "type": "stat",
"content": {
"label": "Revenue",
"source": { "kind": "endpoint", "url": "/api/finance/revenue", "path": "total" },
"format": { "style": "currency", "currency": "@config.currency", "decimals": 0 }
}
}
<CnDashboardPage :widgets="WIDGETS" :layout="layout" :app-config="appConfig" />

A manifest renderer typically seeds appConfig from loadState(appId, 'config', {}). @config.<key> also interpolates into an endpoint source's URL / params and into OpenRegister filter values. It is backwards-compatible: a literal "EUR" still works, and a required @config.<key> that is unset falls back to the format's default (EUR for currency, empty for prefix/suffix) rather than rendering the raw token. A trailing ? marks the token optional (dropped from filters when unset).

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

NameTypeRequiredDefaultDescription
titlestring''Page title
descriptionstring''Page description (shown below title)
widgetsArray<{ 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 } } } }
layoutunion[]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.
contentArray<{ 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/&lt;schemaSlug&gt;/&lt;widgetSlug&gt;' } 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.
bodyWidgetsArray<{id?: string, component: string, title?: string, props?: object, placement?: string, colSpan?: number}>[]Declarative IN-BODY sections (host-app section components) rendered ALONGSIDE the widget grid — the dashboard equivalent of CnDetailPage's bodyWidgets. Each entry: { id?, component, title?, props?, placement?, colSpan? } - component — registry name resolved from the v2 cnRegistry first, then the legacy cnCustomComponents map (the SAME resolver type:"custom" pages use). A bespoke funnel / time-series / channel chart that reads a custom REST endpoint stays a registered component and surfaces here without a rewrite. - props — token-resolved against the page context: @workspace.&lt;key&gt; / @page.&lt;key&gt; (page-level workspace state), @config.&lt;key&gt; (app config), and the time / @me tokens. An unresolved optional token is dropped so the child sees undefined. - placementbefore-grid (above the widget grid) | after-grid (below it) | end (the default; same as after-grid). before-grid renders even when the dashboard has no grid widgets. - colSpan — 1–12 grid span when sections at one placement share a row. Empty / omitted (default []) renders nothing — the current behaviour. The section context ({ register, schema, objectId, workspace, config }) is also provided on cnSectionContext so a section component can inject it instead of taking explicit props. See CnBodySections.
appConfigobject\{\}Page-level app config map exposed to declarative widget / section config via the @config.&lt;key&gt; token (e.g. the reporting currency the setup wizard captures). Provided to descendants on cnAppConfig, so a stat widget's format: { style: 'currency', currency: '@config.currency' } formats with the configured value (falling back to the literal default — EUR — when unset) and an endpoint KPI's URL can interpolate @config.&lt;key&gt;. A manifest renderer typically seeds this from loadState(appId, 'config', {}). Empty (default) leaves every @config.&lt;key&gt; token to fall back to its literal default.
loadingbooleanfalseWhether the dashboard is loading
allowEditbooleanfalseWhether to show the edit toggle button
columnsnumber12Number of grid columns
cellHeightnumber80Grid cell height in pixels
gridMarginnumber12Grid margin in pixels
editLabelstring() =&gt; t('nextcloud-vue', 'Edit')Label for the edit button
doneLabelstring() =&gt; t('nextcloud-vue', 'Done')Label for the done button (when editing)
emptyLabelstring() =&gt; t('nextcloud-vue', 'No widgets configured')Label for the empty state
unavailableLabelstring() =&gt; t('nextcloud-vue', 'Widget not available')Label for unavailable widgets
surfacestring'app-dashboard'Rendering surface forwarded to integration widgets (widgets whose type === 'integration'). Drives the AD-19 surface fallback on resolveWidget(integrationId, surface).
integrationContextunionnullObject 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.
appStatusesunionnullOptional per-app install/enable status map used by the requiresApp widget gate. Shape: { [appId]: { installed: boolean, enabled: boolean, category?: string } } — typically the dependency_statuses initial state a consuming app injects. When a widget definition carries requiresApp (a string or array of app ids) and a required app is not enabled here, that widget renders an "Install …" call-to-action instead of its normal content. When this prop is null the gate falls back to the isAppInstalled() runtime check (OC.appswebroots → capabilities), so the gate works with or without injected data.
dateRangeunionnullOptional 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, control?: 'picker' | 'pills', default?: { from: string, to: string, preset?: string }, persistKey?: string, presets?: Array&lt;{ id: string, label: string, days: number|null }&gt;, } control selects the header control style: the default 'picker' renders the CnDateRangePicker (a preset select + two date inputs); 'pills' renders a compact segmented toggle-button row (one pill per preset, plus a de-emphasised "Custom range" popover pill). Both drive the same shared range. A preset with no numeric days / hours (other than the manual custom entry) — e.g. { id: 'all', label: 'All', days: null } — is an "All" / CLEAR preset: picking it removes the window so date-bound widgets show their unfiltered count. An explicit clear: true forces the same behaviour. The active window is published into the page-level workspace context as dateFrom / dateTo / datePreset, so any declarative widget can scope itself to it via the optional filter tokens @workspace.dateFrom? / @workspace.dateTo? (an empty bound, i.e. the "All" range, drops the token so the filter is omitted). Default preset when no explicit default and no persisted state is found: last-7 (now − 7d → now). Per-widget surfacing: chart widgets with a dataSource.bucket get an in-header date chip automatically; CUSTOM and registered card widgets (stat / gauge / delta KPI tiles) opt in via layout[].dateChip: true (same popover, writes the same shared range — so an abstract KPI card carries its own period selector). The chip needs the widget header (showTitle !== false) and shows the active preset's label (e.g. 7d / All), staying clickable even on an unbounded ("All") range. Set showHeaderPicker: false to rely solely on the per-widget chips.
pageFiltersunion[]Optional page-level filter controls rendered in the dashboard header. Each selection is written into the reactive page-level workspace context (the same bag cnWorkspaceContext provides), so any widget can read it via a @page.&lt;key&gt; / @workspace.&lt;key&gt; token — e.g. a period selector that every endpoint-bound KPI's URL interpolates. Each entry: { key, label?, type?: 'select', options: [{ value, label }], default? }. key is the workspace-context key written on change; default (or the first option) seeds it on mount. Only 'select' is supported today. Empty (the default) renders no controls and leaves the context untouched.
showRefreshbooleantrueShow 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.
showRequestFeaturebooleantrueShow the built-in Request-a-feature item in the page-level overflow Actions menu. On by default; opens the CnSuggestFeatureModal when mounted under CnAppRoot.
documentationUrlstring''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.
documentationLabelstring() =&gt; t('nextcloud-vue', 'Documentation')Pre-translated label for the Documentation action. Defaults to "Documentation".
pageIdstring''Stable id for this dashboard, used in the @refresh / @request-feature payloads and the surface: "dashboard:&lt;id&gt;" field on the feature-request modal. Falls back to a slugified title when unset.
specRefstring''Optional specRef forwarded to the feature-request modal.
refreshingbooleanfalseWhether a page-level refresh is in flight (disables the Refresh item and shows its spinner).
refreshLabelstring() =&gt; t('nextcloud-vue', 'Refresh')Pre-translated label for the Refresh action.
requestFeatureLabelstring() =&gt; t('nextcloud-vue', 'Request a feature')Pre-translated label for the Request-a-feature action.
actionsMenuLabelstring() =&gt; t('nextcloud-vue', 'Actions')Pre-translated aria-label / tooltip for the overflow menu trigger.

Events

NamePayloadDescription
layout-changeEmitted when the user finishes dragging/resizing a widget. Payload: the updated layout array [{ widgetId, x, y, w, h }, ...].
edit-toggleEmitted when the user toggles edit mode. Payload: true when entering edit mode, false when leaving.
date-range-changeFired whenever the dashboard's effective date range changes (initial resolve, picker change, or persisted-range restore). Payload: { from, to, preset }.
page-filter-changeEmitted when a page-level filter selection changes.
refreshundefinedUser 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-featureundefinedUser 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-refreshUser clicked Refresh in a widget's overflow action menu. Payload: the layout item descriptor.
widget-request-featureUser clicked Request a feature in a widget's overflow action menu. Payload: the layout item descriptor.
widget-removeundefinedEmitted when a widget is removed via the in-place editor.

Slots

NameBindingsDescription
header-actionsheader-actions Inline buttons rendered in the dashboard header next to the edit toggle. Used by every existing consumer (decidesk, mydash, opencatalogi, pipelinq, procest).
actionsactions Back-compat alias for #header-actions. Prefer #header-actions in new code.
action-items
emptyempty 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.widgetIdname, item, widgetwidget-{widgetId} Per-widget body content (e.g. #widget-my-work). Apps inject custom widget rendering here. Scope: { item, widget }.