feat(integrations): CnDetailPage sidebar Object form accepts useRegistry / excludeIntegrations (manifest-driven registry sidebar)#218
Conversation
…try / excludeIntegrations (manifest-driven registry sidebar)
Closes the gap found while live-testing the xWiki leaf: a consuming
app whose detail sidebar is manifest-driven (CnDetailPage via
CnAppRoot's #sidebar slot) couldn't flip that sidebar to registry
mode — `useRegistry` was a `CnObjectSidebar` prop only, with no path
from `pages[].config.sidebar`.
- CnDetailPage: the `sidebar` Object form (and `sidebarProps`) now
accept `useRegistry: boolean` and `excludeIntegrations: string[]`;
`mergeSidebarSources()` merges them and `syncSidebarState()`
pushes them into the injected `objectSidebarState`
(`objectSidebarState.useRegistry` / `.excludeIntegrations`), reset
when the page is suppressed. The `sidebar` prop JSDoc documents
the new keys + the mutual exclusion with `tabs` + the host-app
binding requirement.
- app-manifest.schema.json + validateManifest.js: `config.sidebar`
on detail pages may now carry `useRegistry` (boolean) and
`excludeIntegrations` (string[]); validation rejects a non-boolean
`useRegistry`, non-array / non-string `excludeIntegrations`, and
`useRegistry: true` together with `tabs` (mutually exclusive). The
schema descriptions are updated to mention both.
- docs: cn-object-sidebar.md gets a "Registry-driven tabs
(useRegistry)" section including the `#sidebar`-slot binding
snippet and the manifest example; cn-detail-page.md's sidebar
config object section mentions `useRegistry` / `excludeIntegrations`;
CLAUDE.md's "Surface components consume the registry" list grows a
CnDetailPage bullet; CnDetailPage's `_generated` partial regenerated.
- tests: CnDetailPageSidebarConfig.spec.js — 4 new tests (Object
form pushes useRegistry/excludeIntegrations into objectSidebarState;
defaults; cleared on suppression; sidebarProps fallback);
app-manifest.schema.spec.js — 4 new tests (accepts both keys;
rejects non-boolean useRegistry; rejects useRegistry+tabs; rejects
bad excludeIntegrations).
The consuming app still has to bind `:use-registry` /
`:exclude-integrations` from `objectSidebarState` onto its
`#sidebar`-slot `<CnObjectSidebar>` (documented) — that's the one
non-automatic step, the same shape as the existing `:tabs` /
`:hidden-tabs` binding it already does.
Verification: `npx jest` → 72 suites, 1040 passing (4 pre-existing
failures: CnMapWidget + CnAiChatPanel, unrelated); `npm run lint` —
no new errors; `npm run check:jsdoc` → "All 79 components meet their
JSDoc baseline".
…tionsController shape) The AD-23 failure-path contract on OpenRegister's ObjectIntegrationsController surfaces the ProviderUnavailableException cause under `details.cause`, but CnXwikiTab.degradedMessage() only checked `details.reason` / a bare `reason` — so a real "openconnector-source-missing" 503 fell through to the generic "unavailable" banner instead of the more actionable "reconnect connector" one. Accept `details.cause` (preferred) plus the legacy shapes, and treat `openconnector-down` as a reconnect cause too. Tests + the SFC header updated.
…IntegrationsController shape)
OpenRegister's `ObjectIntegrationsController::index` returns `{ items: [...] }`,
but the xwiki tab/card only un-wrapped `{ results: [...] }` (or a bare array) —
so against the real OR endpoint `this.pages` ended up being the envelope object,
`v-for` iterated its values, and the linked-page row rendered blank. Accept
`results` ?? `items` ?? bare-array for both components (list fetch and the
single-entity `loadEntity` lookup). +1 test for the `{ items }` envelope.
There was a problem hiding this comment.
[CONCERN] Vue 2 reactivity: new keys on provided objectSidebarState may not be reactive
CnDetailPage writes objectSidebarState.useRegistry and objectSidebarState.excludeIntegrations by direct property assignment (lines 211-212, 216-217). In Vue 2, adding new properties to an already-reactive plain object bypasses the observation system — downstream template bindings such as :use-registry="objectSidebarState.useRegistry" on the host's <CnObjectSidebar> will NOT update reactively unless the host pre-declared those keys in its data() before providing the object. The test helper makeState() correctly initialises both fields, confirming the authors know about this, but the component itself provides no guard. Fix: document this requirement prominently in the migration note, OR use Vue.set(this.objectSidebarState, 'useRegistry', value) on first write so the component self-heals.
There was a problem hiding this comment.
[CONCERN] sidebarProps fallback for useRegistry / excludeIntegrations is undocumented
The mergeSidebarConfig helper falls back to props.useRegistry (i.e. this.sidebarProps.useRegistry) when the sidebar Object form omits it. The test exercises this code path, but the JSDoc for the sidebarProps prop only lists register, schema, hiddenTabs, title, subtitle — useRegistry and excludeIntegrations are not mentioned. Either update the sidebarProps JSDoc to list the full accepted key set, or explicitly deprecate the sidebarProps path in favour of the Object sidebar form only.
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Review
🟡 Concerns (2)
- Vue 2 reactivity: new keys on provided objectSidebarState may not be reactive —
src/components/CnDetailPage/CnDetailPage.vue:211 - sidebarProps fallback for useRegistry / excludeIntegrations is undocumented —
src/components/CnDetailPage/CnDetailPage.vue:229
🟢 Minor (3)
- No validation warning when excludeIntegrations is set without useRegistry: true (
src/utils/validateManifest.js:349)
excludeIntegrationsis validated for type but there is no warning when it is set withuseRegistryabsent orfalse. In that case the array is forwarded toCnObjectSidebarwhere it has no effect. A console.warn insyncSidebarStatefor this combination would surface the misconfiguration earlier and mirror the style of the existinguseRegistry + tabsmutual-exclusion check. - Nullish coalescing on data.results changes behaviour when results is explicit null (
src/components/CnXwikiCard/CnXwikiCard.vue:258)
The new entity-resolve usesdata.results ?? data.items ?? .... The??operator treatsnullas nullish, so{ results: null, items: [...] }would pick upitems. The previous code (Array.isArray(data.results) === true) returned false fornulland fell through todatadirectly. If any API variant returnsresults: nullas a deliberate 'no results' sentinel, the new code silently changes the resolved entity. - Docs example assumes CnObjectSidebar already accepts useRegistry / excludeIntegrations props (
docs/components/cn-object-sidebar.md:157)
The new docs example shows:use-registryand:exclude-integrationsbound to<CnObjectSidebar>. This PR is stacked onfeature/integration-xwiki. If those props were added toCnObjectSidebarin a prior PR in that chain, the example is correct — but the generated docs (docs/components/_generated/CnObjectSidebar.md) were not updated in this PR, making it impossible to verify from the diff alone. Confirm merge order so host-wiring docs never land onmasterbefore theCnObjectSidebarprop additions do.
Reviewed by WilcoLouwerse via automated batch review.
Closes the gap found while live-testing the xWiki leaf: a consuming app whose detail sidebar is manifest-driven (
CnDetailPageviaCnAppRoot's#sidebarslot) couldn't flip that sidebar to registry mode —useRegistrywas aCnObjectSidebarprop only, with no path frompages[].config.sidebar. Stacked on #213 → #210 → #209 → #204 → #202 →beta.What's in this PR
CnDetailPage— thesidebarObject form (andsidebarProps) now acceptuseRegistry: boolean+excludeIntegrations: string[];mergeSidebarSources()merges them andsyncSidebarState()pushes them into the injectedobjectSidebarState(reset when the page is suppressed). Thesidebarprop JSDoc documents the new keys, the mutual exclusion withtabs, and the host-app binding requirement.app-manifest.schema.json+validateManifest.js—config.sidebaron detail pages may now carryuseRegistry/excludeIntegrations; validation rejects a non-booleanuseRegistry, non-array / non-stringexcludeIntegrations, anduseRegistry: truetogether withtabs(mutually exclusive). Schema descriptions updated.cn-object-sidebar.mdgains a "Registry-driven tabs (useRegistry)" section with the#sidebar-slot binding snippet + a manifest example;cn-detail-page.md's sidebar-config section mentions the new keys;CLAUDE.mdgrows aCnDetailPagebullet;_generated/CnDetailPage.mdregenerated.CnDetailPageSidebarConfig.spec.js+4 (pushes intoobjectSidebarState; defaults; cleared on suppression;sidebarPropsfallback);app-manifest.schema.spec.js+4 (accepts both keys; rejects non-booleanuseRegistry; rejectsuseRegistry+tabs; rejects badexcludeIntegrations).The consuming app still has to bind
:use-registry/:exclude-integrationsfromobjectSidebarStateonto its#sidebar-slot<CnObjectSidebar>(documented) — same shape as the:tabs/:hidden-tabsbinding it already does. That's why my live-test deskdesk rewrite (which only changedApp.vuepartially) didn't flip the sidebar — this PR makes the manifest path actually work end-to-end.Test plan
npx jest→ 72 suites, 1040 passing (4 pre-existing failures:CnMapWidget+CnAiChatPanel, unrelated, also red onorigin/beta)npm run lint— no new errors (7 remaining are pre-existing inuseObjectLock.js)npm run check:jsdoc→ "All 79 components meet their JSDoc baseline"node scripts/check-docs.js— only the pre-existing CnAi*/useAi*/defaultgaps