feat(integrations): registry-aware CnObjectSidebar / CnDashboardPage / CnDetailPage + referenceType on CnFormDialog / CnDetailGrid#209
Conversation
… PR 8 — part 1)
Opt-in `useRegistry` prop on CnObjectSidebar: when `true`, the
hardcoded built-in tab set (Files/Notes/Tags/Tasks/AuditTrail) is
replaced by one tab per provider registered on
`window.OCA.OpenRegister.integrations` (consumed via
`useIntegrationRegistry()`).
Backwards-compatible: the legacy hardcoded branch and the open-enum
`tabs` branch are untouched. `useRegistry` is mutually exclusive with
`tabs` — when both are set, `tabs` wins and a console.warn fires.
Per umbrella change `pluggable-integration-registry`:
- AD-9 / AD-13: `excludeIntegrations` prop (mirrors `hiddenTabs`)
filters the registry set; `hiddenTabs` ids are also honoured
- registry is reactive — late registrations re-render the sidebar
- `#extra-tabs` slot still appends consumer-supplied tabs
- per-integration slot overrides aren't supported in registry mode
(apps override a built-in by registering their own provider with
the same id; collision policy: first wins)
Also: `useIntegrationRegistry` now uses `shallowRef` instead of `ref`
for the snapshot — the descriptor objects hold Vue component options
in `tab`/`widget`/`widget*`, and deep reactive observation would tag
those component objects with `__ob__` and break `<component :is>`
resolution.
Implements part of tasks 9.1–9.6 (CnObjectSidebar; CnDashboardPage /
CnDetailPage / CnFormDialog / CnDetailGrid follow in the same PR
branch).
Files:
- src/components/CnObjectSidebar/CnObjectSidebar.vue — new registry
branch + `useRegistry` / `excludeIntegrations` props +
`isRegistryMode` / `filteredRegistryIntegrations` computeds +
`resolveRegistryTab()` method + collision warning at mount
- src/composables/useIntegrationRegistry.js — `ref` → `shallowRef`
- tests/components/CnObjectSidebar.spec.js — 6 new tests for
registry mode (one tab per provider, excludeIntegrations,
hiddenTabs, reactive late registration, #extra-tabs slot,
collision fallback warning)
Tests: 23 CnObjectSidebar tests + 21 registry/composable tests, all green.
Lint: no new warnings (the 2 remaining order-in-components warnings on
CnObjectSidebar pre-date this change).
…tailPage (umbrella PR 8 — part 2)
Adds a `type: 'integration'` widget kind to both surface components.
A widget def `{ id, title, type: 'integration', integrationId, props? }`
resolves its Vue component from the pluggable integration registry,
applying the AD-19 surface fallback via
`resolveWidget(integrationId, surface)`.
CnDashboardPage:
- new `surface` prop (default `'app-dashboard'`) + `integrationContext`
prop (`{ register, schema, objectId }`)
- `setup()` wires `useIntegrationRegistry`
- `isIntegration` / `resolveIntegrationWidget` / `getIntegrationProps`
methods + a new dispatcher branch (before the unknown fallback)
that renders the widget inside the existing `CnWidgetWrapper`, or
the `unavailableLabel` when the integration isn't registered
CnDetailPage:
- new `surface` prop (default `'detail-page'`) + `integrationContext`
prop (falls back to deriving `{register, schema, objectId}` from
`sidebarProps` + `objectId`)
- `setup()` adds `resolveRegistryWidget`
- `isIntegrationWidget` / `resolveIntegrationWidget` /
`getIntegrationProps` methods
- the grid `#widget-<id>` slot now has fallback content: when the
widget def is `type: 'integration'`, render the registry widget on
the `detail-page` surface; a consumer-supplied slot still overrides
Backwards-compatible: all existing widget types (custom slot, tile,
chart, stats-block, NC API) are untouched; `surface` /
`integrationContext` are optional with sensible defaults.
Implements tasks 9.2–9.3 of the umbrella.
Tests (10 new, all green):
- CnDashboardPage.spec.js — 5 integration-widget tests (resolve from
registry, surface + integrationContext forwarding, per-widget props
merge, unavailable fallback, missing integrationId → unknown)
- CnDetailPageIntegrationWidget.spec.js — 4 tests (detail-page surface
render, explicit integrationContext override, unregistered → nothing
extra, consumer slot overrides the fallback)
Full touched-component suite: 6 suites, 59 tests, green.
…og + CnDetailGrid (umbrella PR 8 — part 3)
Implements AD-18 for the form/detail surfaces: a schema property (or
detail item) carrying `referenceType: '<integration-id>'` renders the
integration's single-entity widget (AD-19 fallback to its main
`widget`) instead of a plain input / value.
- `utils/schema.js` — `fieldsFromSchema()` now passes `referenceType`
through onto the field descriptor (was previously stripped)
- `CnFormDialog.vue` — `setup()` wires `useIntegrationRegistry`; new
`referenceContext` prop (`{ register, schema, objectId }`);
`resolveReferenceWidget(field)` / `referenceWidgetProps(field)`
methods; a new field-dispatch branch (before the auto-generated
inputs) rendering `<component :is>` for `referenceType` fields and
emitting `@input` → `updateField`. A consumer `#field-<key>` slot
still overrides it.
- `CnDetailGrid.vue` — same treatment for `items[].referenceType`:
`setup()` + `referenceContext` prop + `resolveReferenceWidget(item)`
/ `referenceWidgetProps(item)` methods; the value `<slot>` now has
fallback content rendering the integration widget. A consumer
`#item-<index>` slot still overrides it.
- `CLAUDE.md` — documents the surface-component consumption (CnObjectSidebar
`useRegistry`, the `integration` widget type on CnDashboardPage/
CnDetailPage, `referenceType` on CnFormDialog/CnDetailGrid)
Backwards-compatible: all new props are optional; when no integration
matches a `referenceType` the surfaces render exactly as before.
Implements tasks 9.4–9.6 of the umbrella. Completes the PR-8 set
(9.1–9.6).
Tests (8 new + 1 new suite, all green):
- CnFormDialog.spec.js — 3 referenceType tests (renders the
single-entity widget, forwards referenceContext, falls back to the
plain field when no integration registered)
- CnDetailGrid.spec.js (new suite) — 5 tests (renders the widget,
forwards referenceContext, plain value when unregistered, plain
value without referenceType, consumer #item-<index> slot override)
Full suite: 70 suites, 1003 passing, 4 pre-existing failures
(CnMapWidget + CnAiChatPanel, unrelated to this change, also failing
on origin/beta).
…nceContext props
`@type {?{ register?: string, schema?: string, objectId?: string }}` —
the inline record syntax with a leading `?` — is not valid TypeScript,
and `vue-docgen-api` (which check:jsdoc and the `prebuild:docs`
auto-doc generation both rely on) fails to parse it, taking down the
whole JSDoc-ratchet + auto-doc-partials CI gate. Switched the four
new props to `@type {object|null}` (the convention already used by
CnAppRoot / CnAppNav / CnObjectSidebar); the prose above each prop
already documents the `{ register, schema, objectId }` shape.
Affects the four surface components touched in this PR:
CnDashboardPage, CnDetailPage, CnFormDialog, CnDetailGrid.
…re surface components PR 8 added the `useRegistry` / `surface` / `integrationContext` / `referenceContext` props (and `referenceType` field handling) to CnObjectSidebar, CnDashboardPage, CnDetailPage, CnFormDialog and CnDetailGrid; this regenerates their `docs/components/_generated/*.md` partials via `cd docusaurus && npm run prebuild:docs` so the "Auto-doc partials are fresh" CI gate passes. No source change — just the vue-docgen output catching up to the props added earlier in this PR.
There was a problem hiding this comment.
[CONCERN] Late-registration reactivity gap: resolveIntegrationWidget is not reactive
The template uses v-if="resolveIntegrationWidget(item)" and :is="resolveIntegrationWidget(item)", which internally calls a plain function returned from useIntegrationRegistry(). This function performs a live registry lookup but is NOT a computed property, so Vue 2 does not track the registry state as a dependency. If an integration registers after the component mounts (late registration), the template won't re-evaluate and the widget stays in the fallback state.
Contrast with CnObjectSidebar, which correctly exposes registryIntegrations (a reactive computed wrapping the shallowRef snapshot).
Fix: compute resolvedIntegrationWidgets as a computed map from (widgetId → component) that reads this.registryIntegrations, so Vue tracks the dependency. The same issue exists in CnDetailPage.vue (line 553), CnDetailGrid.vue (line 429), and CnFormDialog.vue (line 708).
There was a problem hiding this comment.
[CONCERN] objectType used as schema fallback in derivedContext — semantically wrong
getIntegrationProps constructs a derived context where schema: resolved.schema || this.objectType || this.sidebarProps?.schema || ''. objectType is the entity-type slug (e.g. 'lead'), while schema is expected to be an OpenRegister schema UUID/identifier. Passing the object-type slug where integration widgets expect a schema ID will cause those widgets to make API requests with an invalid schema identifier, silently returning wrong data. The fallback should stop at this.sidebarProps?.schema and log a warning if neither is available.
There was a problem hiding this comment.
[CONCERN] Integration widget @input contract is undocumented and untested end-to-end
The CnFormDialog expects integration widgets to emit input with the raw new value. There is no test that mounts a widget, fires @input, and asserts that formData[field.key] is updated. The prop contract (referenceWidgetProps) does not document that the widget MUST emit input (not update:value). If the widget emits a native DOM InputEvent object rather than the plain value, updateField will receive the event object.
Fix: add a test for the round-trip and document the required event signature in referenceWidgetProps JSDoc.
There was a problem hiding this comment.
[CONCERN] useRegistry=true with empty registry renders sidebar with zero tabs — no UX fallback
When useRegistry is true and no integrations are registered (race condition on page load or mis-configured consumer), filteredRegistryIntegrations is [], so the registry branch renders no tabs at all — a blank sidebar. There is no test covering this scenario and no documented behaviour. Consider whether a fallback to built-in tabs or an emptyRegistryLabel slot is desired when the registry is empty.
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Review
🟡 Concerns (4)
- Late-registration reactivity gap: resolveIntegrationWidget is not reactive —
src/components/CnDashboardPage/CnDashboardPage.vue:289 - objectType used as schema fallback in derivedContext — semantically wrong —
src/components/CnDetailPage/CnDetailPage.vue:684 - Integration widget @input contract is undocumented and untested end-to-end —
src/components/CnFormDialog/CnFormDialog.vue:711 - useRegistry=true with empty registry renders sidebar with zero tabs — no UX fallback —
src/components/CnObjectSidebar/CnObjectSidebar.vue:822
🟢 Minor (4)
typeof getRegistryIntegration === 'function'guard is always true — dead code (src/components/CnDetailGrid/CnDetailGrid.vue:512)
getRegistryIntegrationis always a function (set fromuseIntegrationRegistry()insetup()). Thetypeofguard adds noise and never protects anything. The same dead guard appears inCnFormDialog.vueline 779. Remove thetypeofcheck.- INTEGRATION_SURFACES constant duplicated in two component files (
src/components/CnDashboardPage/CnDashboardPage.vue:308)
INTEGRATION_SURFACESis defined identically at module scope in bothCnDashboardPage.vueandCnDetailPage.vue. If a new surface is added, both files must be updated in sync. Export the constant fromuseIntegrationRegistry.jsor a sharedintegrations/constants.jsand import it in both components. - resolveIntegrationWidget(item) called twice per render cycle in template (
src/components/CnDashboardPage/CnDashboardPage.vue:289)
The template evaluatesresolveIntegrationWidget(item)twice per layout item per render (once forv-if, once for:is). For dashboards with many integration widgets, this doubles registry lookup cost. The same pattern appears inCnDetailPage.vueandCnDetailGrid.vue. Fix: use a single computed orv-forhelper variable that pre-resolves the component. - Missing test: @input from referenceType widget updates formData (
tests/components/CnFormDialog.spec.js:1302)
The newCnFormDialog — referenceTypetest suite verifies that the integration widget renders and receives props correctly, but never exercises the round-trip: widget emitsinput→updateFieldis called → formData is updated → form submits the new value. Add a test that emitsinputfrom the widget component and assertswrapper.vm.formData[field.key]is updated.
Reviewed by WilcoLouwerse via automated batch review.
Part of the openregister pluggable-integration-registry umbrella. Cross-repo PR 8/N. Stacked on #204 (the 3 parity widgets) → #202 (JS registry) →
beta; GitHub auto-retargets as upstream merges.Summary
Wires the four surface components to the pluggable integration registry. Implements tasks 9.1–9.6 of the umbrella. Everything is opt-in and backwards-compatible — every new prop defaults to today's behaviour and no existing test changed.
CnObjectSidebar(9.1)useRegistry(Boolean, defaultfalse): replace the hardcoded built-in tabs (Files/Notes/Tags/Tasks/AuditTrail) with one tab per provider registered onwindow.OCA.OpenRegister.integrationsexcludeIntegrations(string[]) + existinghiddenTabsfilter the registry set; registry is reactive (late registrations re-render)tabsprop —tabswins, with a console.warn#extra-tabsslot still appends consumer tabs; per-integration slot overrides aren't supported in registry mode (override a built-in by registering your own provider with the same id — collision policy: first wins)CnDashboardPage+CnDetailPage(9.2–9.3)integrationwidget type:{ id, title, type: 'integration', integrationId, props? }— component resolved viaresolveWidget(integrationId, surface)(AD-19 fallback)surfaceprop (default'app-dashboard'/'detail-page') +integrationContextprop ({ register, schema, objectId }) forwarded to the widget;CnDetailPagederivesintegrationContextfromsidebarProps+objectIdwhen not givenCnDashboardPage: new dispatcher branch inside the existingCnWidgetWrapper, orunavailableLabelwhen the integration isn't registeredCnDetailPage: the grid#widget-<id>slot now has fallback content rendering the registry widget; a consumer slot still overridesCnFormDialog+CnDetailGrid+utils/schema.js(9.4–9.6, AD-18)fieldsFromSchema()now passesreferenceTypethrough onto the field descriptor (was stripped)referenceType: '<integration-id>'renders that integration's single-entity widget (AD-19 fallback to its mainwidget) instead of a plain input/valuereferenceContextprop ({ register, schema, objectId }) forwarded; a consumer#field-<key>/#item-<index>slot still overridesCLAUDE.mddocuments the surface-component consumptionImplementation note
useIntegrationRegistrynow usesshallowRefinstead ofreffor its snapshot — the descriptor objects hold Vue component options intab/widget/widget*, and deep reactive observation would tag those component objects with__ob__and break<component :is>resolution.Test plan
CnMapWidget+CnAiChatPanel, unrelated, also failing onorigin/beta)npm run lint— no new errors; new warnings are limited toprops-above-setuporder-in-components hints, consistent with the existingCnObjectSidebar/CnDetailPagepattern