Ga naar hoofdinhoud

CnWidgetObjectTable

CnWidgetObjectTable is the built-in v2 widget that exposes CnDataTable to the manifest layer. All data props and listeners are forwarded; since ADR-049 (list-widget-enrichment) it additionally carries a declarative self-fetching source, the compact list surface, and declarative row/widget actions[] — so a fleet dashboard list can be expressed entirely as a manifest widget entry with no bespoke component.

Import​

import { CnWidgetObjectTable } from '@conduction/nextcloud-vue'

Manifest usage​

Referenced via widgetKey: "object-table" in a v2 manifest page's widgets[] array:

{
"id": "my-cases",
"slot": "body",
"widgetKey": "object-table",
"gridX": 0, "gridY": 0, "gridWidth": 6, "gridHeight": 6,
"props": {
"source": {
"register": "@resolve:tenant_register",
"schema": "case",
"filter": { "assignee": "@me", "status": "@workspace.openStatus?" },
"order": { "dueDate": "asc" },
"limit": 5
},
"columns": [
{ "key": "title", "label": "Case" },
{ "key": "dueDate", "label": "Due", "formatter": "daysUntil" }
],
"hideHeader": true,
"rowRoute": "case-detail",
"viewAllRoute": { "name": "cases" },
"emptyText": "No open cases",
"actions": [
{ "id": "accept", "label": "Accept", "type": "object-op", "op": "patch", "values": { "status": "accepted" } },
{ "id": "remove", "label": "Remove", "type": "object-op", "op": "delete" },
{ "id": "add", "label": "New case", "type": "object-op", "op": "create", "values": { "status": "open" } }
]
}
}

When CnPageRenderer mounts the page, CnWidgetGrid resolves the object-table key against BUILT_IN_WIDGETS, instantiates this wrapper, and passes the props map through.

Declarative source (self-fetch)​

source ({ register, schema, filter, order, limit, extend }, default null) drives CnDataTable's existing self-fetch — the widget does not re-implement fetching:

  • filter is token-resolved with the shared resolveFilterTokens grammar just before fetching: @me, @today, @workspace.<key>, @objectId / @object.<field> (on detail pages). A ?-suffixed optional token that resolves to empty drops its clause (dropOptionalUnresolved) instead of failing the fetch; an unresolved required token skips the fetch entirely. Since Wave 3 the @objectId / @object.<field> tokens resolve from either detail surface: CnDetailPage's cnObjectContext ref (which wins per field) or CnPageRenderer's v2 cnDetailObjectContext holder (which backfills), so a ZGW-style sidebar tab widget works without a CnDetailPage ancestor.
  • register MAY carry an @resolve: sentinel — the widget passes it through unexpanded (resolution is the host loader's job).
  • order becomes _order[field]=asc|desc fetch params; limit caps the rendered rows (the widget fetches limit + 1 so the "View all" footer can detect that more rows exist).
  • extend (Wave 3, #91) — an array of OpenRegister _extend[] values (e.g. ["calculations"]) forwarded on the fetch, so virtual / declarative calc fields (procest daysOverdue / daysUntilDeadline) ride along and render as ordinary columns.
  • Externally supplied rows always win — passing rows/columns without a source behaves exactly as before.

Endpoint binding (Wave 2, #91)​

endpointSource ({ url, method?, params?, responsePath? }, default null) is the alternative to the OpenRegister source for rows an app REST endpoint computes (e.g. a per-source performance report):

{
"props": {
"endpointSource": {
"url": "/apps/pipelinq/api/reports/source-performance",
"params": { "from": "@workspace.dateFrom?", "to": "@workspace.dateTo?" },
"responsePath": "report.sources"
},
"columns": [
{ "key": "source", "label": "Source" },
{ "key": "conversionRate", "label": "Conversion %" }
],
"rowRoute": "lead-detail"
}
}
  • Rows come from the payload at responsePath (dot-path; the plucked value must be an array — anything else renders as empty).
  • params use the shared filter-token grammar and re-resolve + refetch when the page context changes; fetching goes through the shared useEndpointSource engine (request dedup + short-TTL cache, cn:page:refresh / cn:widget:refresh wiring on the widget's widgetId).
  • Exactly one of source | endpointSource (validator-enforced); external rows still win over both (and suppress the request).
  • columns / formatters / rowRoute / actions apply unchanged on top of endpoint rows.

Declarative actions​

actions[] (default []) takes the unified manifest action shape (handler | open-modal | open-page | navigate | object-op):

  • Non-mutating types and object-op patch / delete render per row through CnRowActions (delete styled destructive).
  • object-op create renders as a widget-scoped footer affordance (there is no row to mutate) and creates against the widget source.
  • delete is always confirm-gated through CnConfirmDialog; patch / create confirm only when the action sets confirm: true.
  • Mutations dispatch via the shared object store (dispatchAction — object-op). The manifest declares intent only: authorization-shaped fields have no effect, OpenRegister RBAC is the authority, and a rejected write surfaces an inline error with no local mutation. After a successful mutation the self-fetch refreshes and an object-op event is emitted (so hosts feeding external rows can refetch).

Props​

PropTypeDescription
sourceObjectDeclarative self-fetch source { register, schema, filter, order, limit } (default null).
endpointSourceObjectEndpoint-bound rows { url, method, params, responsePath } (default null) — see Endpoint binding. Exactly one of source | endpointSource.
actionsArrayDeclarative actions (unified action shape incl. object-op, default []).
registerStringRegister slug for self-fetch mode (legacy direct prop).
schemaStringSchema slug for self-fetch mode (legacy direct prop).
columnsArrayColumn definitions — bare string keys or the object form ({ key, label, sortable, width, cellClass, formatter, widget, format, aggregate }).
rowsArrayPre-fetched rows — always win over source (disables self-fetch).
loadingBooleanLoading flag forwarded to CnDataTable's skeleton state.
hideHeaderBooleanHide the column-header row (compact list surface, default false).
borderlessBooleanDrop CnDataTable's card chrome (default false).
rowRouteStringRoute NAME for row-click navigation — mapped to CnDataTable's rowClickRoute with { params: { id } }.
viewAllRouteObjectvue-router location for the "View all" footer link (default null).
viewAllLabelStringPre-translated "View all" label (CnDataTable default when unset).
emptyTextStringEmpty-state text (CnDataTable default when unset).
rowIconString | FunctionLeading per-row icon: MDI name or (row) => iconName (default null).
rowClassFunction | ArrayPer-row CSS class binding (default null) — a host-supplied (row) => string function (pass-through), or a declarative rules[] array compiled here into that function. See Declarative rowClass.
titleStringWidget title shown in the chrome header (default 'Table').
documentation-urlStringDocumentation link for the overflow Actions menu (default '').
widget-idStringStable id forwarded to the widget chrome (default '').
hideWrapperBooleanRender content-only, without the widget's own CnWidgetWrapper chrome (default false). Set by hosts that already provide the card chrome — CnDashboardPage's registry branch mounts the widget this way to avoid a double card.

All other props are forwarded to CnDataTable — see the CnDataTable docs for the full surface.

Declarative rowClass (#91)​

rowClass gives a declarative way to highlight rows from the manifest — overdue / at-risk rows get a CSS class without a bespoke render function. It accepts two forms:

  • a host-supplied function (row) => string, passed straight through to CnDataTable's own rowClass (the pre-existing contract), or
  • an array of rules [{ when: { field, op?, value }, class }], compiled here into that function. Each rule adds its class to a row when the shared visibleWhen predicate (LOCAL form) holds against the row: field is a dot-path into the row, op is eq | neq | gt | gte | lt | lte (default eq), value is the literal right-hand side. Rules evaluate in order and every matching class is space-joined.
{
"props": {
"source": { "register": "procest", "schema": "case" },
"columns": [{ "key": "title", "label": "Case" }],
"rowClass": [
{ "when": { "field": "status", "op": "eq", "value": "overdue" }, "class": "row--overdue" },
{ "when": { "field": "daysUntilDeadline", "op": "lt", "value": 3 }, "class": "row--at-risk" }
]
}
}

The app styles .row--overdue / .row--at-risk in its own stylesheet.

Dashboard registration (type: "object-table")​

Besides the v2 grid (widgetKey: "object-table"), the widget is registered in the shared dashboardWidgetRegistry so a CnDashboardPage widget definition with type: "object-table" renders it — including through the in-app "Add widget…" picker (the config form is the shared object-list form: register / schema / filters / sort / limit / columns). The registered renderer is a chrome-aware adapter: it mounts the widget with hideWrapper: true inside the dashboard's own CnWidgetWrapper, and accepts the stored content blob in either the flat form shape ({ register, schema, filter, sort, limit, columns }) or the v2 prop shape ({ source, columns, actions, … }).

Slots​

Every host-supplied CnDataTable scoped slot is forwarded verbatim — notably #footer ({ total, shown }), #empty, and #column-<key>. The #row-actions slot is withheld while the widget renders its own declarative actions[] menu.

Events​

All listeners are forwarded to CnDataTable (@row-click, @sort, etc.). The widget itself emits:

EventPayloadDescription
object-op{ action, row, result }After a successful declarative mutation — hosts feeding external rows refetch on it.

Spec​

  • REQ-MVR-006 (manifest-v2-renderer) — built-in widget: object-table
  • list-widget-enrichment (ADR-049 Decision 2) — declarative source, actions, object-op, compact list surface