feat(integration-xwiki): CnXwikiTab + CnXwikiCard + xwiki registration (leaf, frontend)#213
feat(integration-xwiki): CnXwikiTab + CnXwikiCard + xwiki registration (leaf, frontend)#213rubenvdlinde wants to merge 5 commits into
Conversation
…f, frontend) The @conduction/nextcloud-vue half of the `integration-xwiki` leaf (openregister issue #1326) — the JS twin of the PHP XwikiProvider. - src/components/CnXwikiTab/ — sidebar "Articles" tab: lists the linked XWiki pages with their full breadcrumb (AD-3), a link form that accepts a full XWiki URL or a `Space.Page` path (AD-2), an unlink button (removes the OR sub-resource pairing only — never deletes the page in XWiki), and a reconnect/unavailable banner when the endpoint answers 503 (AD-23). Emits `linked` / `unlinked`. - src/components/CnXwikiCard/ — surface-aware widget (AD-19): on `detail-page` the linked list plus a TEXT preview of the first page (HTML stripped to plain text, ~500 chars, macros NOT executed — AD-1) plus an "Open in XWiki" link; on `user-dashboard` / `app-dashboard` a compact list; on `single-entity` a title + breadcrumb chip resolved from the `value` prop (for `referenceType: 'xwiki'` properties), with a minimal-chip fallback when the lookup fails. A 503 renders a quiet unavailable state. - src/integrations/builtin/xwiki.js — `xwikiIntegration` descriptor (id `xwiki`, group `external`, requiredApp `openconnector`, referenceType `xwiki`, tab + widget) + `registerXwikiIntegration()`. This is a LEAF, not a built-in: it is not in `builtinIntegrations` and `registerBuiltinIntegrations()` does not register it — OpenRegister's bundle calls `registerXwikiIntegration()` explicitly (skip-on-collision: a consuming app may pre-register `xwiki` to override it). - barrels (`src/components/index.js`, `src/index.js`, `src/integrations/index.js`) export `CnXwikiTab`, `CnXwikiCard`, `xwikiIntegration`, `registerXwikiIntegration` - docs: `docs/components/cn-xwiki-tab.md`, `cn-xwiki-card.md`, `docs/utilities/xwiki-integration.md`, `register-xwiki-integration.md` - tests: `tests/components/CnXwikiTab.spec.js` (7 — list+breadcrumb, empty, link-by-URL POST, unlink DELETE on the OR sub-resource, 503→reconnect banner, 503→generic banner), `CnXwikiCard.spec.js` (8 — compact list, maxDisplay cap, detail-page text preview with HTML stripped + no macro execution, ~500-char truncation, single-entity chip from `value`, fallback chip, 503 unavailable, empty), `tests/integrations/xwiki.spec.js` (6 — descriptor shape, registerXwikiIntegration registers + skip-on-collision + not part of the built-in set + defaults to singleton) Implements the frontend tasks of the integration-xwiki leaf. Tests: 21 new (CnXwikiTab + CnXwikiCard + xwiki registration), all green. Full suite: 74 suites, 1032 passing, 4 pre-existing failures (CnMapWidget + CnAiChatPanel, unrelated, also red on origin/beta). npm run lint — no new errors/warnings on the xWiki files. check-docs — only pre-existing CnAi*/useAi*/default gaps. Note: the auto-generated `docs/components/_generated/CnXwikiTab.md` and `CnXwikiCard.md` partials still need regenerating (`cd docusaurus && npm run prebuild:docs`) — docusaurus deps weren't installed in this environment; same situation as CnFilesCard/CnTagsCard/ CnAuditTrailCard from #204.
…+ spec deviations
Syncs the integration-xwiki leaf tasks.md to reality:
- backend (XwikiProvider + DI registration + source-config template
+ user guide + unit tests) — done in this branch
- frontend (CnXwikiTab + CnXwikiCard + registration + tests) — done
in ConductionNL/nextcloud-vue#213
- acceptance: link-by-URL / reference-property / "macros not
executed in preview" are covered by the component tests; the live
E2E (against a running XWiki via an OpenConnector source) is
deferred until the umbrella + leaf PRs merge and deploy
Spec deviations recorded inline:
- `requiredApp` is `'openconnector'` (not `null`) — more accurate
for the admin UI: the integration genuinely needs that app
- the OpenConnector source-config template lives at
`docs/Integrations/xwiki-openconnector-source.yaml`, not
`config/openconnector-sources/xwiki.yaml` (the repo's `config/`
dir is gitignored)
- the provider is registered via `addProvider()` at boot (not a DI
tag) — consistent with the umbrella's registration mechanism
- `check-integration-parity.js` (Node, repo convention) not `.sh`
…als for CnXwikiTab/CnXwikiCard
`check:jsdoc` requires new components to score 100%. Both CnXwikiTab
and CnXwikiCard had props grouped under a single `/** Pre-translated
labels. */` comment (only the first prop in each group counted) and
CnXwikiTab's `linked` / `unlinked` events were undocumented.
- CnXwikiTab: each label prop now has its own JSDoc; `linked` and
`unlinked` get a `/** @event … */` immediately before their
`this.$emit(…)` call (the placement vue-docgen-api attaches event
descriptions to)
- CnXwikiCard: `noPagesLabel` / `openInXwikiLabel` /
`unavailableLabel` each get their own JSDoc
- generated `docs/components/_generated/{CnXwikiTab,CnXwikiCard}.md`
via `cd docusaurus && npm run prebuild:docs`
- `scripts/.jsdoc-baselines.json` records both at 100%
(`npm run jsdoc-baselines:update`)
(This commit also carries forward — via the merge from
feature/integration-builtins — the JSDoc/parser fixes already landed
on the parent branches: the parser-safe `@type {object|null}` on the
PR-8 surface-component props, the `show-all` event docs on
CnFilesCard/CnAuditTrailCard, and their regenerated partials.)
Verification: `npm run check:jsdoc` → "All 79 components meet their
JSDoc baseline" (exit 0); `npx jest tests/components/CnXwiki*.spec.js
tests/integrations/xwiki.spec.js` → 21 tests green; `npm run lint` —
clean on the xWiki files.
…th (/integrations/xwiki), not /xwiki
`CnXwikiTab` and `CnXwikiCard` were hitting
`/api/objects/{r}/{s}/{id}/xwiki`, but the umbrella's registry-driven
sub-resource (handled by `ObjectIntegrationsController`) lives at
`/api/objects/{r}/{s}/{id}/integrations/{integrationId}` — i.e.
`.../integrations/xwiki`. (The `{integrationId}` is namespaced under
`/integrations/` so it can't collide with the legacy
`/objects/{r}/{s}/{id}/{type}` sub-resources for files/notes/etc.)
The old path would have 404'd against a live deployment.
- `CnXwikiTab.baseUrl()` / `CnXwikiCard.baseUrl()` →
`${apiBase}/objects/${register}/${schema}/${objectId}/integrations/xwiki`
- updated the component header comments to reference
`ObjectIntegrationsController` + the correct path
- updated the two URL assertions in `CnXwikiTab.spec.js` /
`CnXwikiCard.spec.js` accordingly
Verification: `npx jest tests/components/CnXwiki*.spec.js
tests/integrations/xwiki.spec.js` → 21 green; `npm run lint` clean
on the xWiki files; `npm run check:jsdoc` → "All 79 components meet
their JSDoc baseline".
There was a problem hiding this comment.
[CONCERN] HTML tag-stripping regex is bypassable — script body text may appear in preview
The previewText computed strips <script> only via /\<script[\s\S]*?\<\/script>/gi. Because the final output is rendered via {{ }} (text interpolation), Vue itself escapes it — so there is no XSS risk in the DOM — but the raw script body text will appear in the visible preview if the regex is bypassed by exotic variants. A safer approach is to parse through a temporary DOM element (el.textContent) or use a well-tested sanitiser. At minimum add a second /\<script[^>]*>[\s\S]*?\<\/script>/gi pass before the generic tag-strip.
There was a problem hiding this comment.
[CONCERN] unlink(): no guard on falsy page.id — DELETE fires against empty/undefined path
The unlink call uses encodeURIComponent(page.id). There is no validation that page.id is a non-empty string before the call; if the API returns an object with a falsy or missing id, the URL becomes .../xwiki/ (or .../xwiki/undefined) and a spurious DELETE fires. Add a guard: if (!page?.id) { return } before this.unlinkingId = page.id.
There was a problem hiding this comment.
[CONCERN] unlink() silently swallows non-2xx, non-503 errors — no user feedback on failure
When the DELETE returns a status that is neither ok nor 503 (e.g. 403, 404, 500), the code falls through without setting any error state and without removing the row. The user clicks the unlink button, nothing changes, no feedback is shown. Add an else branch that sets an appropriate error state so the user knows the action failed.
There was a problem hiding this comment.
[CONCERN] Watch on surface triggers load() without cancelling in-flight requests — race condition
When surface changes while a loadList() or loadEntity() is in progress, the new load() call starts a second fetch. Both resolve independently and the last one to settle wins, potentially replacing the correct result with stale data. The component should use an AbortController keyed to the current load, or at minimum track a generation counter and discard responses from superseded loads (const gen = ++this._loadGen; ... if (gen !== this._loadGen) return).
There was a problem hiding this comment.
[CONCERN] Full page content fetched on all surfaces — over-fetch on non-detail surfaces
On the detail-page surface, loadList() sends _limit = maxDisplay (default 5), but previewText only needs the first page's content field. On other surfaces, content is never read but is still fetched. Consider sending _fields=id,title,url,space,breadcrumb on non-detail-page surfaces to avoid transferring unused payload on dashboards loading multiple widgets simultaneously.
There was a problem hiding this comment.
[CONCERN] NcTextField uses :value.sync — may not match actual NcTextField contract
<NcTextField :value.sync="linkInput" ...> relies on the child emitting update:value. In @nextcloud/vue v8+ NcTextField uses a standard v-model (emits input, not update:value). If the installed version does not emit update:value, the binding becomes one-way and linkInput will never be updated by the user's typing, so submitLink() will always receive an empty string. Change to v-model="linkInput" or verify the NcTextField version supports .sync. The test circumvents this by directly setting wrapper.vm.linkInput, so CI does not catch the bug.
There was a problem hiding this comment.
[CONCERN] fetchPages() not re-triggered when register or schema props change
Only objectId is watched. If a parent component changes register or schema while objectId stays the same (e.g., navigating between schemas in the same object view), the list is not refreshed. Add watchers for register and schema similar to the objectId watcher, or combine them into a single computed key and watch that.
There was a problem hiding this comment.
[CONCERN] xwikiIntegration.label evaluated at module load time — translation may not be ready
label: t('nextcloud-vue', 'Articles') is evaluated when the module is first imported, which can be before translations are loaded. Consider making label a getter or a function, mirroring the approach used for prop defaults (default: () => t(...)).
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Review
🟡 Concerns (8)
- HTML tag-stripping regex is bypassable — script body text may appear in preview —
src/components/CnXwikiCard/CnXwikiCard.vue:427 - unlink(): no guard on falsy page.id — DELETE fires against empty/undefined path —
src/components/CnXwikiTab/CnXwikiTab.vue:897 - unlink() silently swallows non-2xx, non-503 errors — no user feedback on failure —
src/components/CnXwikiTab/CnXwikiTab.vue:891 - Watch on
surfacetriggers load() without cancelling in-flight requests — race condition —src/components/CnXwikiCard/CnXwikiCard.vue:447 - Full page content fetched on all surfaces — over-fetch on non-detail surfaces —
src/components/CnXwikiCard/CnXwikiCard.vue:479 - NcTextField uses :value.sync — may not match actual NcTextField contract —
src/components/CnXwikiTab/CnXwikiTab.vue:684 - fetchPages() not re-triggered when register or schema props change —
src/components/CnXwikiTab/CnXwikiTab.vue:802 - xwikiIntegration.label evaluated at module load time — translation may not be ready —
src/integrations/builtin/xwiki.js:84
🟢 Minor (4)
- breadcrumbText() called multiple times per render — should be computed (
src/components/CnXwikiCard/CnXwikiCard.vue:284)
In the single-entity template,breadcrumbText(entity)is called twice (:titleattribute and interpolation). In the list template it is called twice per row. AdisplayedPagescomputed that pre-computes breadcrumbs would be cleaner and avoid redundant calls. - Hardcoded hex fallback color — may be inconsistent with Nextcloud 28+ warning palette (
src/components/CnXwikiTab/CnXwikiTab.vue:927)
background: var(--color-warning, #e9a40f)— the hex is Nextcloud 25 amber; Nextcloud 28+ changed the warning palette slightly. Prefervar(--color-warning)only (without fallback) if the minimum supported Nextcloud version guarantees the variable, to avoid colour inconsistency in dark mode. - baseUrl() is a method but should be a computed — redundant string building on every call (
src/components/CnXwikiCard/CnXwikiCard.vue:454)
baseUrl()concatenatesapiBase,register,schema, andobjectIdon every invocation. Since these are all props, acomputed: { baseUrl() { ... } }would cache the value and is more idiomatic in Vue 2. Same issue exists inCnXwikiTab. - Test directly mutates vm.linkInput — bypasses NcTextField binding contract (
tests/components/CnXwikiTab.spec.js:1323)
wrapper.vm.linkInput = 'https://wiki.example.org/...'sets data directly, skipping the UI interaction. This means the test would pass even if the NcTextField binding is broken (see the CONCERN about:value.sync). Prefer triggering actual input events on the text field to catch binding regressions.
Reviewed by WilcoLouwerse via automated batch review.
The
@conduction/nextcloud-vuehalf of theintegration-xwikileaf (openregister issue #1326, depends_onpluggable-integration-registry) — the JS twin of the PHPXwikiProvider(openregister PR for it lands the backend). Stacked on #210 (built-in registrations) → #209 → #204 → #202 →beta.Summary
The worked external-storage example for the pluggable integration registry.
CnXwikiTab— sidebar "Articles" tab: linked XWiki pages with their full breadcrumb (AD-3), a link form accepting a full XWiki URL or aSpace.Pagepath (AD-2), an unlink button (removes the OR sub-resource pairing only — never deletes the page in XWiki), a reconnect/unavailable banner on a 503 (AD-23). Emitslinked/unlinked.CnXwikiCard— surface-aware widget (AD-19):detail-page— linked list + a text preview of the first page (HTML stripped to plain text, ~500 chars, macros not executed — AD-1) + an "Open in XWiki" linkuser-dashboard/app-dashboard— compact listsingle-entity— a title + breadcrumb chip resolved from thevalueprop (forreferenceType: 'xwiki'properties), with a minimal-chip fallback when the lookup failssrc/integrations/builtin/xwiki.js—xwikiIntegrationdescriptor +registerXwikiIntegration(). Leaf, not a built-in: not inbuiltinIntegrations, not registered byregisterBuiltinIntegrations()— OpenRegister's bundle callsregisterXwikiIntegration()explicitly (skip-on-collision so a consuming app can overridexwiki).Test plan
npx jest tests/integrations/xwiki.spec.js tests/components/CnXwiki*.spec.js→ 21 tests, greenCnMapWidget+CnAiChatPanel, unrelated, also red onorigin/beta)npm run lint— no new errors/warnings on the xWiki filesnode scripts/check-docs.js— only pre-existing CnAi*/useAi*/defaultgapsdocs/components/_generated/CnXwikiTab.md/CnXwikiCard.mdneed regenerating (cd docusaurus && npm run prebuild:docs) — docusaurus deps weren't installed in the dev env; same asCnFilesCard/CnTagsCard/CnAuditTrailCardfrom feat(integrations): CnFilesCard + CnTagsCard + CnAuditTrailCard widgets #204xwikisource configured, the "Articles" tab/widget should populate and the detail-page text preview should render (with no macros executed)