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 |
pageFilters | Array | [] | 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
| 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) |
flush | Boolean | Whether 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. |
dateChip | Boolean | Show the per-widget date-range chip (see Per-card date chip) |
showActions | Boolean | Forwarded to CnWidgetWrapper — set false to drop the header overflow menu on compact tiles |
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. |
page-filter-change | { key, value } | Emitted when a page filter selection changes. The value is also written into the reactive workspace context. |
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, - 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
| Field | Type | Description |
|---|---|---|
enabled | Boolean | When true, renders the picker row. When false / omitted, no row appears. |
control | String (opt.) | Header control style: 'picker' (default — CnDateRangePicker: a preset select + two date inputs) or 'pills' (a compact segmented toggle-button row). |
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. 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
@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.
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 }
]
}
}
placement—before-grid(above the grid, renders even when there are no grid widgets),after-grid(below it), orend(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. apageFiltersselection),@config.<key>(app config, see below), plus the time /@me/@objectIdtokens. An unresolved optional token (@workspace.<key>?) is dropped so the child seesundefined.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
| 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. | |
bodyWidgets | Array<{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.<key> / @page.<key> (page-level workspace state), @config.<key> (app config), and the time / @me tokens. An unresolved optional token is dropped so the child sees undefined. - placement — before-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. | |
appConfig | object | \{\} | Page-level app config map exposed to declarative widget / section config via the @config.<key> 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.<key>. A manifest renderer typically seeds this from loadState(appId, 'config', {}). Empty (default) leaves every @config.<key> token to fall back to its literal default. | |
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. | |
appStatuses | union | null | Optional 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. | |
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, control?: 'picker' | 'pills', default?: { from: string, to: string, preset?: string }, persistKey?: string, presets?: Array<{ id: string, label: string, days: number|null }>, } 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. | |
pageFilters | union | [] | 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.<key> / @workspace.<key> 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. | |
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 (disables the Refresh item and shows its spinner). | |
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 }. |
page-filter-change | — | Emitted when a page-level filter selection changes. |
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. |
widget-remove | undefined | Emitted when a widget is removed via the in-place editor. |
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 }. |