Skip to content

feat(integration-xwiki): CnXwikiTab + CnXwikiCard + xwiki registration (leaf, frontend)#213

Open
rubenvdlinde wants to merge 5 commits into
feature/integration-builtinsfrom
feature/integration-xwiki
Open

feat(integration-xwiki): CnXwikiTab + CnXwikiCard + xwiki registration (leaf, frontend)#213
rubenvdlinde wants to merge 5 commits into
feature/integration-builtinsfrom
feature/integration-xwiki

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

The @conduction/nextcloud-vue half of the integration-xwiki leaf (openregister issue #1326, depends_on pluggable-integration-registry) — the JS twin of the PHP XwikiProvider (openregister PR for it lands the backend). Stacked on #210 (built-in registrations) → #209#204#202beta.

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 a Space.Page path (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). Emits linked / 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" link
    • user-dashboard / app-dashboard — compact list
    • single-entity — a title + breadcrumb chip resolved from the value prop (for referenceType: 'xwiki' properties), with a minimal-chip fallback when the lookup fails
    • 503 → quiet unavailable state
  • src/integrations/builtin/xwiki.jsxwikiIntegration descriptor + registerXwikiIntegration(). Leaf, not a built-in: not in builtinIntegrations, not registered by registerBuiltinIntegrations() — OpenRegister's bundle calls registerXwikiIntegration() explicitly (skip-on-collision so a consuming app can override xwiki).
  • barrels + 4 doc files

Test plan

  • npx jest tests/integrations/xwiki.spec.js tests/components/CnXwiki*.spec.js → 21 tests, 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
  • node scripts/check-docs.js — only pre-existing CnAi*/useAi*/default gaps
  • docs/components/_generated/CnXwikiTab.md / CnXwikiCard.md need regenerating (cd docusaurus && npm run prebuild:docs) — docusaurus deps weren't installed in the dev env; same as CnFilesCard/CnTagsCard/CnAuditTrailCard from feat(integrations): CnFilesCard + CnTagsCard + CnAuditTrailCard widgets #204
  • Live smoke-test after the chain merges: with OpenConnector installed + an xwiki source configured, the "Articles" tab/widget should populate and the detail-page text preview should render (with no macros executed)

…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.
rubenvdlinde added a commit to ConductionNL/openregister that referenced this pull request May 12, 2026
…+ 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".
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] 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.

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

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

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

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

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

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

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] 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(...)).

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 (8)

🟢 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 (:title attribute and interpolation). In the list template it is called twice per row. A displayedPages computed 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. Prefer var(--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() concatenates apiBase, register, schema, and objectId on every invocation. Since these are all props, a computed: { baseUrl() { ... } } would cache the value and is more idiomatic in Vue 2. Same issue exists in CnXwikiTab.
  • 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.

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