feat(integrations): CnFilesCard + CnTagsCard + CnAuditTrailCard widgets#204
feat(integrations): CnFilesCard + CnTagsCard + CnAuditTrailCard widgets#204rubenvdlinde wants to merge 2 commits into
Conversation
Three surface-aware compact widgets that fill the parity gap for the
five built-in integrations (files, tags, audit-trail). Wraps the
existing sub-resource endpoints (/objects/{register}/{schema}/{id}/files,
/tags, /audit-trail) and renders inside CnDetailCard.
Per umbrella change `pluggable-integration-registry` AD-19, each
widget handles all four surfaces — user-dashboard, app-dashboard,
detail-page, single-entity — from a single component (the `surface`
prop is accepted and validated; a future surface-specific override
can be added at registration time via widgetCompact/widgetExpanded/
widgetEntity without touching these components).
Implements tasks 8.1–8.4 of the umbrella.
Components:
- src/components/CnFilesCard/ — name + size rows, Show-all overflow
(emits show-all), formatFileSize helper (B / KB / MB / GB),
fetch-failure fallback to empty state
- src/components/CnTagsCard/ — inline pills for fetched system tags
- src/components/CnAuditTrailCard/ — action / actor / when rows
with formatActor + formatWhen fallback chain (actorDisplayName ->
actor -> userId -> user), Show-all overflow, fetch-failure
fallback to empty state
Each widget:
- Accepts register/schema/objectId/surface props (validator on
surface against VALID_SURFACES)
- Fetches via the existing REST endpoint using buildHeaders()
- Refetches when objectId changes (watch immediate)
- Renders inside CnDetailCard for consistent styling
- Emits show-all when overflow control is clicked (where applicable)
Barrels & docs:
- src/components/index.js + src/index.js — public exports
- docs/components/cn-files-card.md, cn-tags-card.md,
cn-audit-trail-card.md — Playground + usage + reference (matches
the existing card doc template)
- Also fills the docs gap from PR 6 (umbrella PR 6/N):
docs/utilities/integrations.md
docs/utilities/create-integration-registry.md
docs/utilities/install-integration-registry.md
docs/utilities/v-a-l-i-d_-s-u-r-f-a-c-e-s.md
docs/utilities/composables/use-integration-registry.md
Tests (12 new, all green):
- tests/components/CnFilesCard.spec.js — empty state, rendered rows
capped at maxDisplay, show-all emission, refetch on objectId
change, fetch-failure fallback
- tests/components/CnTagsCard.spec.js — empty state, pill rendering,
API-error fallback
- tests/components/CnAuditTrailCard.spec.js — empty state, row
capping + Show-all, show-all emission, actor-fallback chain
Quality:
- npx jest tests/components/Cn{Files,Tags,AuditTrail}Card.spec.js
-> 3 suites, 12 tests, all passing
- npm run lint — no new errors or warnings on the files added by
this PR
- node scripts/check-docs.js — 8 remaining gaps are all pre-existing
(CnAi* family + a `default` export leak unrelated to this work)
…TrailCard + generate doc partials `check:jsdoc` requires new components to score 100% — both CnFilesCard and CnAuditTrailCard were missing JSDoc on their `show-all` event (vue-docgen-api picks up the template `$emit` site but with no description). Moved the emit into an `onShowAll()` method and put a `/** @event show-all … */` JSDoc immediately before the `this.$emit('show-all')` call (the placement vue-docgen-api attaches event descriptions to, same as CnDataTable's `select-all`). The template `@click` now calls `onShowAll`. Also: - generated the missing `docs/components/_generated/{CnFilesCard, CnTagsCard,CnAuditTrailCard}.md` partials (via `cd docusaurus && npm run prebuild:docs`) so the "Auto-doc partials are fresh" CI gate passes for these components - bumped `scripts/.jsdoc-baselines.json` to record the 3 new components at 100% (`npm run jsdoc-baselines:update`) Verification: `npm run check:jsdoc` → "All 77 components meet their JSDoc baseline" (exit 0); `npx jest tests/components/Cn{Files,Tags, AuditTrail}Card.spec.js` → 12 tests green (the show-all click test still passes — the `@click` just routes through `onShowAll` now).
There was a problem hiding this comment.
[BLOCKER] Unvalidated file.url used as href — open redirect / javascript: injection
The file.url value comes directly from the API response and is bound to :href without any validation. A malicious or compromised API response could return javascript:alert(1) or a phishing redirect. Fix: validate that the URL starts with a safe protocol before rendering the anchor, e.g. a computed helper isSafeUrl(url) { try { const u = new URL(url, location.origin); return ['http:', 'https:'].includes(u.protocol) } catch { return false } }. Use v-if="isSafeUrl(file.url)" to gate the anchor element, falling back to the plain <span> branch that already exists.
There was a problem hiding this comment.
[CONCERN] API limit param makes 'Show all' overflow button unreachable
The fetch passes limit: String(this.maxDisplay) to the endpoint, so data.results will contain at most maxDisplay items. The overflow footer condition files.length > maxDisplay will therefore never be true — the server already truncated the list. Either (a) fetch limit + 1 to detect overflow, or (b) rely on the response's pagination metadata (data.total or data.count). The same pattern exists in CnAuditTrailCard.
There was a problem hiding this comment.
[CONCERN] limit param makes the 'Show all' overflow button unreachable (audit trail)
Same issue as CnFilesCard: params = new URLSearchParams({ limit: String(this.maxDisplay) }) limits server response to exactly maxDisplay rows. The footer condition entries.length > maxDisplay can never be true. Fix: fetch maxDisplay + 1 items, render only maxDisplay, and show the overflow button when the extra item is present.
There was a problem hiding this comment.
[CONCERN] Silent non-2xx responses show empty state — fetch errors invisible to user
When response.ok is false (4xx/5xx), entries is set to [] silently. The user sees 'No audit entries yet' — indistinguishable from a genuinely empty audit trail. Add an error data flag, set it when !response.ok, and render a distinct error message so users know a fetch failure occurred. The same pattern applies to CnFilesCard and CnTagsCard.
There was a problem hiding this comment.
[CONCERN] Silent non-2xx responses show empty state — fetch errors invisible to user (tags)
Same issue as CnAuditTrailCard: HTTP errors collapse silently into the empty-state label. A 403 or 500 from the tags endpoint will show 'No tags' rather than a meaningful error. Add an error reactive flag and a distinct error message.
There was a problem hiding this comment.
[CONCERN] No maxDisplay / pagination guard — unbounded tag list renders all items
CnFilesCard and CnAuditTrailCard both honour a maxDisplay prop and truncate the rendered list. CnTagsCard has no maxDisplay prop and renders every tag returned by the API. If an object has dozens of tags, the pill list will overflow the card with no overflow control or 'show all' button. Add a maxDisplay prop (defaulting to e.g. 10) mirroring the other two widgets, slice the list in a displayedTags computed, and add a footer overflow control.
There was a problem hiding this comment.
[CONCERN] objectId watcher does not cancel in-flight fetch — race condition on rapid prop change
When objectId changes rapidly, a new fetchFiles() is started before the previous request settles. If the first response arrives after the second, files will be set to stale data. Fix using AbortController: store a controller in data, abort it at the start of fetchFiles(), create a fresh one, and pass { signal } to fetch(). The same pattern applies to CnAuditTrailCard and CnTagsCard.
There was a problem hiding this comment.
[CONCERN] objectId watcher does not cancel in-flight fetch — race condition (audit trail)
Same race condition as CnFilesCard: stale data from an earlier request can overwrite the correct result when objectId changes. Use an AbortController stored in component data, aborting the previous request at the start of each fetchEntries() call.
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Review
🔴 Blockers (1)
- Unvalidated file.url used as href — open redirect / javascript: injection —
src/components/CnFilesCard/CnFilesCard.vue:773
🟡 Concerns (7)
- API limit param makes 'Show all' overflow button unreachable —
src/components/CnFilesCard/CnFilesCard.vue:888 - limit param makes the 'Show all' overflow button unreachable (audit trail) —
src/components/CnAuditTrailCard/CnAuditTrailCard.vue:635 - Silent non-2xx responses show empty state — fetch errors invisible to user —
src/components/CnAuditTrailCard/CnAuditTrailCard.vue:643 - Silent non-2xx responses show empty state — fetch errors invisible to user (tags) —
src/components/CnTagsCard/CnTagsCard.vue:1116 - No maxDisplay / pagination guard — unbounded tag list renders all items —
src/components/CnTagsCard/CnTagsCard.vue:1023 - objectId watcher does not cancel in-flight fetch — race condition on rapid prop change —
src/components/CnFilesCard/CnFilesCard.vue:869 - objectId watcher does not cancel in-flight fetch — race condition (audit trail) —
src/components/CnAuditTrailCard/CnAuditTrailCard.vue:617
🟢 Minor (3)
- d.toLocaleString() uses browser locale — inconsistent with Nextcloud's locale (
src/components/CnAuditTrailCard/CnAuditTrailCard.vue:664)
new Date(raw).toLocaleString()picks up the OS/browser locale rather than Nextcloud's configured language. Use@nextcloud/moment(moment(raw).format('LLL')) or the NextcloudformatDateutility to keep date formatting consistent with the rest of the UI. - formatFileSize uses binary divisors but SI (KB/MB/GB) labels (
src/components/CnFilesCard/CnFilesCard.vue:908)
The labels say 'KB', 'MB', 'GB' but divide by powers of 1024 (binary kibibytes). Use either the correct SI labels ('KiB', 'MiB', 'GiB') for binary, or switch to powers of 1000 for SI. Better: delegate to@nextcloud/filesformatFileSizeto match Nextcloud's own display. - Two $nextTick calls are fragile — use flushPromises() instead (
tests/components/CnAuditTrailCard.spec.js:1229)
Tests awaitwrapper.vm.$nextTick()twice to let the fetch promise and reactivity update settle. Useawait flushPromises()from@vue/test-utilsinstead, which drains the entire microtask queue and is independent of internal implementation details.
Reviewed by WilcoLouwerse via automated batch review.
Part of the openregister pluggable-integration-registry umbrella. Stacked on #202 (JS integration registry); GitHub will auto-retarget to
betaafter #202 merges.Summary
Three surface-aware compact widgets that fill the parity gap for the five built-in integrations (
files,tags,audit-trail). PR 5 already ships rich sidebar tabs for these — this PR adds the widget half of the parity contract so dashboards and detail pages have something to render too.Per AD-19 (surface fallback), each widget handles all four surfaces (
user-dashboard,app-dashboard,detail-page,single-entity) from a single component. Thesurfaceprop is accepted and validated; a future surface-specific override can be added at registration time viawidgetCompact/widgetExpanded/widgetEntitywithout touching these components.Implements tasks 8.1–8.4 of the umbrella.
What's in this PR
Components (all wrap
CnDetailCardfor consistent styling)CnFilesCard— name + size rows, Show-all overflow, formatFileSize helper (B / KB / MB / GB), fetch-failure fallback to empty stateCnTagsCard— inline pills for fetched system tagsCnAuditTrailCard— action / actor / when rows with actor fallback chain (actorDisplayName→actor→userId→user), Show-all overflow, fetch-failure fallback to empty stateEach widget:
register/schema/objectId/surfaceprops (surfacevalidator runs againstVALID_SURFACES)buildHeaders()objectIdchangesshow-allwhen the overflow control is clicked (where applicable)Barrels & docs
src/components/index.js+src/index.js— public exportsdocs/components/cn-files-card.md,cn-tags-card.md,cn-audit-trail-card.md— matches the existing card doc templateintegrations,createIntegrationRegistry,installIntegrationRegistry,VALID_SURFACES,useIntegrationRegistryTests (12 new, all green)
CnFilesCard.spec.js— empty state, rows capped atmaxDisplay,show-allemission, refetch onobjectIdchange, fetch-failure fallbackCnTagsCard.spec.js— empty state, pill rendering, API-error fallbackCnAuditTrailCard.spec.js— empty state, row capping + Show-all,show-allemission, actor-fallback chainTest plan
npx jest tests/components/Cn{Files,Tags,AuditTrail}Card.spec.js→ 3 suites, 12 tests, all passingnpm run lint— no new errors or warnings on the files added by this PRnode scripts/check-docs.js— 8 remaining gaps are all pre-existing (CnAi* family + adefaultexport leak unrelated to this work)CnObjectSidebar/CnDashboardPage/CnDetailPagerefactor) and PR 9 lands (the 5 built-in JS registrations that wire these widgets onto each surface)