Skip to content

feat(integrations): registry-aware CnObjectSidebar / CnDashboardPage / CnDetailPage + referenceType on CnFormDialog / CnDetailGrid#209

Open
rubenvdlinde wants to merge 6 commits into
feature/integration-widgetsfrom
feature/integration-surfaces
Open

feat(integrations): registry-aware CnObjectSidebar / CnDashboardPage / CnDetailPage + referenceType on CnFormDialog / CnDetailGrid#209
rubenvdlinde wants to merge 6 commits into
feature/integration-widgetsfrom
feature/integration-surfaces

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

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, default false): replace the hardcoded built-in tabs (Files/Notes/Tags/Tasks/AuditTrail) with one tab per provider registered on window.OCA.OpenRegister.integrations
  • excludeIntegrations (string[]) + existing hiddenTabs filter the registry set; registry is reactive (late registrations re-render)
  • mutually exclusive with the open-enum tabs prop — tabs wins, with a console.warn
  • #extra-tabs slot 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)

  • new integration widget type: { id, title, type: 'integration', integrationId, props? } — component resolved via resolveWidget(integrationId, surface) (AD-19 fallback)
  • surface prop (default 'app-dashboard' / 'detail-page') + integrationContext prop ({ register, schema, objectId }) forwarded to the widget; CnDetailPage derives integrationContext from sidebarProps + objectId when not given
  • CnDashboardPage: new dispatcher branch inside the existing CnWidgetWrapper, or unavailableLabel when the integration isn't registered
  • CnDetailPage: the grid #widget-<id> slot now has fallback content rendering the registry widget; a consumer slot still overrides

CnFormDialog + CnDetailGrid + utils/schema.js (9.4–9.6, AD-18)

  • fieldsFromSchema() now passes referenceType through onto the field descriptor (was stripped)
  • a property/item carrying referenceType: '<integration-id>' renders that integration's single-entity widget (AD-19 fallback to its main widget) instead of a plain input/value
  • referenceContext prop ({ register, schema, objectId }) forwarded; a consumer #field-<key> / #item-<index> slot still overrides
  • CLAUDE.md documents the surface-component consumption

Implementation note

useIntegrationRegistry now uses shallowRef instead of ref for its 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.

Test plan

  • New tests: 6 (CnObjectSidebar registry mode) + 5 (CnDashboardPage integration widget) + 4 (CnDetailPageIntegrationWidget) + 3 (CnFormDialog referenceType) + 5 (CnDetailGrid referenceType) — all green
  • Full suite: 70 suites, 1003 passing, 4 pre-existing failures (CnMapWidget + CnAiChatPanel, unrelated, also failing on origin/beta)
  • npm run lint — no new errors; new warnings are limited to props-above-setup order-in-components hints, consistent with the existing CnObjectSidebar / CnDetailPage pattern
  • Smoke-test in a live Nextcloud after PR 9 lands (the 5 built-in JS registrations that wire the actual widgets onto each surface)

… 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.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown

@WilcoLouwerse WilcoLouwerse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

🟡 Concerns (4)

🟢 Minor (4)

  • typeof getRegistryIntegration === 'function' guard is always true — dead code (src/components/CnDetailGrid/CnDetailGrid.vue:512)
    getRegistryIntegration is always a function (set from useIntegrationRegistry() in setup()). The typeof guard adds noise and never protects anything. The same dead guard appears in CnFormDialog.vue line 779. Remove the typeof check.
  • INTEGRATION_SURFACES constant duplicated in two component files (src/components/CnDashboardPage/CnDashboardPage.vue:308)
    INTEGRATION_SURFACES is defined identically at module scope in both CnDashboardPage.vue and CnDetailPage.vue. If a new surface is added, both files must be updated in sync. Export the constant from useIntegrationRegistry.js or a shared integrations/constants.js and import it in both components.
  • resolveIntegrationWidget(item) called twice per render cycle in template (src/components/CnDashboardPage/CnDashboardPage.vue:289)
    The template evaluates resolveIntegrationWidget(item) twice per layout item per render (once for v-if, once for :is). For dashboards with many integration widgets, this doubles registry lookup cost. The same pattern appears in CnDetailPage.vue and CnDetailGrid.vue. Fix: use a single computed or v-for helper variable that pre-resolves the component.
  • Missing test: @input from referenceType widget updates formData (tests/components/CnFormDialog.spec.js:1302)
    The new CnFormDialog — referenceType test suite verifies that the integration widget renders and receives props correctly, but never exercises the round-trip: widget emits inputupdateField is called → formData is updated → form submits the new value. Add a test that emits input from the widget component and asserts wrapper.vm.formData[field.key] is updated.

Reviewed by WilcoLouwerse via automated batch review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants