This repository was archived by the owner on May 29, 2026. It is now read-only.
feat: migrate decidesk to JSON manifest renderer (decidesk-manifest-v1)#160
Merged
Conversation
…manifest-v1)
Rewrites src/manifest.json from 20-of-20 type:"custom" page entries
(each pointing at a per-page Vue file) to declarative built-in page
types consumed by @conduction/nextcloud-vue's JSON manifest renderer.
Page-type breakdown after migration:
- 1 dashboard (Dashboard, with widgets[] + layout[])
- 8 index (GovernanceBodies, Meetings, Participants, AgendaItems,
Motions, Minutes, Decisions, ActionItems — each schema-bound with
columns[] and sidebar config)
- 9 detail (matching *Detail pages plus AmendmentDetail — each with
sidebarTabs[] per the abstract-sidebar contract)
- 2 custom survivors:
- LiveMeeting (genuine realtime exception)
- Settings (lib gap: no register-mapping field type yet)
Manifest version bumped 0.4.0 -> 1.0.0. The new manifest is
forward-compatible with three sibling @conduction/nextcloud-vue
changes (manifest-page-type-extensions, manifest-abstract-sidebar,
manifest-config-defs) that are committed but not yet released.
Per-page Vue files for the 18 migrated pages stay in place with TODO
markers; deletion is deferred to the follow-up adoption commit that
swaps the shell to CnAppRoot once @conduction/nextcloud-vue ships
the sibling changes.
Adds tests/validate-manifest.js, an Ajv-based validator that runs
against the v1.1.0 schema. Validation passes with zero errors.
OpenSpec change folder rewritten:
- proposal.md: scope narrowed to per-page-Vue -> manifest-renderer
migration; v1.0.0 stabilisation gates parked.
- design.md: full per-page mapping table, sidebar tab inventory,
custom-fallback inventory split into genuine exceptions / lib
gaps / migration cost.
- tasks.md: this-commit tasks marked [x]; adoption follow-up tasks
marked [ ] with "blocked on lib release" notes.
- specs/decidesk-manifest-v1/spec.md: 8 REQ-DMV1-* requirements.
Out of scope (parked for follow-up changes):
- @conduction/nextcloud-vue floor bump (lib not released yet).
- CnAppRoot adoption / router rewrite / per-page Vue deletion
(follow-up "decidesk-manifest-v1-adopt" commit).
- Multi-tenancy / i18n / resolver consumer wiring.
…s to rich sections Builds on the manifest-rewrite parent commit (b5c88cd). Replaces the legacy Vue shell with the `@conduction/nextcloud-vue` v1.x manifest renderer pattern and lands the Settings page migration that was previously a lib-gap blocker. Changes ------- * src/main.js — derives vue-router routes from manifest.pages[*]; mounts every route with CnPageRenderer; calls registerIcons + registerTranslations. * src/App.vue — replaced MainMenu + <router-view> with <CnAppRoot> driving the full shell (loading / dependency-check / shell phases). Provides the objectSidebarState channel and slots a single host CnObjectSidebar via the #sidebar slot. * src/customComponents.js — new registry exporting LiveMeetingView (genuine realtime exception) plus 9 detail-tab custom components for the cross-schema relations the abstract-sidebar contract resolves at runtime (GovernanceBodyMembersTab, MeetingAgendaTab, MeetingParticipantsTab, AgendaMotionsTab, MotionAmendmentsTab, MotionVotesTab, AmendmentParentMotionTab, MinutesSignersTab, DecisionActionItemsTab). SettingsView is NO LONGER in the registry — see below. * src/components/tabs/*.vue — 9 stub components (each renders a CnNoteCard placeholder + a TODO comment listing the data shape the full version needs). Stubs unblock the manifest cleanup; full implementations land in follow-up tickets. * src/manifest.json — Settings page migrated from type:"custom" to type:"settings" with three rich sections: Version (version-info widget), Registers (register-mapping widget covering all 17 decidesk types), Advanced (flat fields[] for ori_endpoint + email_voting_enabled). The recipe matches manifest-settings-rich-sections/design.md verbatim. * src/views/{18 files}.vue — deleted (the TODO-marked obsolete views from parent commit). src/views/SettingsView.vue, src/router/index.js, and src/navigation/MainMenu.vue also deleted (now redundant). * package.json — @conduction/nextcloud-vue floor bumped from ^0.1.0-beta.17 to ^1.0.0-beta.2. Placeholder pre-release tag matching the consolidated nextcloud-vue feature/manifest-v1 worktree's package.json. Bump to the actual published semver once the lib v1.x ships to npm. * tests/validate-manifest.js — schema-lookup order extended to find the v1.2.0 schema in the consolidated manifest-v1 worktree (in addition to the previous v1.1.0 page-type-extensions worktree). * openspec/changes/decidesk-manifest-v1/{design,tasks}.md — Cleanup follow-up section marked DONE; per-page mapping table updated for Settings (custom → settings); §7 tasks marked complete. Validation ---------- * `node tests/validate-manifest.js` against schema v1.2.0: PASS (0 errors). * `npx eslint src/{main.js,App.vue,customComponents.js,components/tabs/*.vue}`: clean (0 errors / 0 warnings). * Full Nextcloud regression / Playwright smoke is intentionally NOT run here — @conduction/nextcloud-vue v1.x is upstream and not yet published to npm. Runtime adoption tests follow the lib release. Custom-fallback inventory (where custom remains) ------------------------------------------------ LiveMeetingView — only genuine type:"custom" survivor. Realtime meeting shell with WebSocket subscriptions / per-frame UI; no abstract analogue. Documented as the canonical example for a future type:"realtime" lib extension. 9 detail-tab components — stubs today (CnNoteCard placeholders), tracked as TODO(decidesk-manifest-v1-tab) in each file. Full versions need to fetch related objects through useObjectStore('decidesk-objects') and render via CnDataTable / CnCardGrid; deferred to per-tab follow-up tickets. Settings — formerly the second custom survivor; migrated this commit. Blocked / deferred ------------------ * Lib v1.x publish: package.json carries the placeholder ^1.0.0-beta.2 (matching the consolidated worktree's declared version). Bump on publish. * Runtime smoke / Playwright regression: gated on the lib publish. * Dashboard widget runtime check: if the published widget registry doesn't ship a `stats-block` widget type, downgrade Dashboard to type:"custom" in a small follow-up commit (per design.md open-question 1). * Orphan components (AmendmentList.vue, MeetingLifecycle.vue, VotingRoundPanel.vue, GlobalSearch.vue) have no live importers post- cleanup. Out of scope for this commit; future dead-code sweep. Refs ---- * openspec/changes/decidesk-manifest-v1/design.md — "Cleanup follow-up" * nextcloud-vue/openspec/changes/manifest-settings-rich-sections/design.md * hydra/openspec/architecture/adr-024-app-manifest.md
Replaces the 9 placeholder CnNoteCard stubs in src/components/tabs/
with full implementations against @conduction/nextcloud-vue v1.x
patterns: useObjectStore for cross-schema fetches, CnDataTable for
listings, CnFormDialog / CnDeleteDialog for authoring.
Tabs by posture
---------------
Full CRUD (list / add / edit / delete via row click + actions):
* MeetingAgendaTab — agenda-item where meeting === parent.id
* AgendaMotionsTab — motion where agendaItem === parent.id
* MotionAmendmentsTab — amendment where parentMotion === parent.id
* DecisionActionItemsTab — action-item where decision === parent.id
Add-existing / remove (link existing records, no authoring):
* GovernanceBodyMembersTab — participant.governanceBody pointer
* MeetingParticipantsTab — participant.meetings[] array
* MinutesSignersTab — minutes.signers[] array + "Sign now" CTA
via /api/minutes/:id/transition
Read-only (display + click-through):
* MotionVotesTab — motion → voting-round → vote audit listing.
Vote authoring stays on LiveMeeting.
* AmendmentParentMotionTab — single parent-motion summary card +
"View motion" link.
Helper
------
* src/components/tabs/useRelationStore.js — small factory exposing
ensureRelationType(slug). Lazily registers child object types with
the lib's useObjectStore using schema slugs sourced from the
settings store, falling back to the literal slug when settings
haven't loaded yet.
OpenSpec
--------
* openspec/changes/decidesk-manifest-v1/tasks.md — adds §7b with
one [x] task per tab and the helper module.
* openspec/changes/decidesk-manifest-v1/design.md — Cleanup follow-up
section now reflects that the 9 tabs are full implementations,
no longer stubs.
Validation
----------
* node tests/validate-manifest.js → PASS (manifest unchanged)
* npx eslint src/components/tabs/ → 0 errors, 0 warnings
* npm run lint → 0 errors (39 pre-existing
warnings in unrelated files)
… alias
The manifest v1 work merged on the lib's beta branch and was published
as @conduction/nextcloud-vue@1.0.0-beta.5 (npm dist-tag 'beta'). This
commit pulls it in, completing the decidesk-manifest-v1 migration:
- package.json: ^1.0.0-beta.2 -> ^1.0.0-beta.5
- package-lock.json: regenerated against the new lib
Verified against the published lib (NOT against a local worktree):
- Manifest schema validation (Ajv, draft 2020-12) PASSES against
schema v1.2.0 shipped in the npm tarball.
- ESLint clean across the new tabs (src/components/tabs/), the
customComponents registry (src/customComponents.js), and the
manifest-driven shell (src/main.js, src/App.vue).
- Webpack production build succeeds (3 warnings, no errors).
Webpack alias added for @nextcloud/axios. The package's exports field
declares only the 'import' condition, but @nextcloud/vue's CJS bundle
calls require('@nextcloud/axios'), which webpack 5 resolves under the
'require' condition. The alias points the bare specifier directly at
the dist entry, bypassing the exports-field gate. Use the $-suffixed
exact-match form so subpath imports keep their normal resolution.
This is a transient interop issue between two upstream Nextcloud
packages — not a manifest-migration concern — but lands here so
decidesk builds cleanly today.
Contributor
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 425/425 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Coverage: 0% (0/115 statements)
Quality workflow — 2026-05-09 12:45 UTC
Download the full PDF report from the workflow artifacts.
l10n ---- - Add 88 Dutch translations for the keys introduced by the new sidebar tabs (Members, Motions, Amendments, Signers, Votes, Action items + verbs add/edit/delete/sign + loading/empty/error states). - Mirror the same keys into en.json (English-as-source-of-truth, same shape as the existing entries). MotionVotesTab — voter hydration -------------------------------- The vote audit list now resolves the raw `caster` foreign-key to the participant's display name. New `hydrateCasters()` runs once per refresh: collects unique caster ids, fetches the matching participant records via `useObjectStore`, builds an id → name map. The table's `#column-caster` slot reads that map; falls back to the raw value (or "—") when a participant can't be resolved (deleted / not in this register). Non-fatal — a fetch failure leaves the column showing the raw caster value. customComponents.js — comment refresh ------------------------------------- Replaced the stale "stubs today; flesh out under follow-up tickets" comment that no longer matches reality (all 9 tabs are full-implementation now).
Contributor
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 425/425 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Coverage: 0% (0/115 statements)
Quality workflow — 2026-05-09 13:26 UTC
Download the full PDF report from the workflow artifacts.
Three fixes from the localhost:8080 browser smoke test that are keepable independent of the still-open Vue.extend / frozen-component issue (see the PR description for that). main.js — robust translation loading ------------------------------------- Decouple Vue mount from translation load. Some Nextcloud installs (the standard openregister dev container included) only allow JS/CSS through Apache static-serve — `/custom_apps/<app>/l10n/<locale>.json` 404s. `@nextcloud/l10n`'s `loadTranslations` rejects on 404, so wrapping the mount inside its callback meant any non-Apache-served locale silently killed the app's bootstrap. Now: kick off translation load fire-and-forget; mount Vue regardless; strings fall back to English source on miss. Also catches synchronous throws from `registerTranslations()` so a lib-side translation hiccup can't crash boot. main.js — clone CnPageRenderer for Vue Router --------------------------------------------- Lib barrel exports are non-extensible (webpack ESM module records). Vue Router calls `Vue.extend(component)` on each route's component, which tries to attach a `_Ctor` cache; that throws against non-extensible imports. Shallow-clone CnPageRenderer once at bootstrap and pass the clone to every route — extensible options object, no mutation of the lib's internals. (Same pattern applied to defaultPageTypes and customComponents passed as props.) l10n/en_US.json --------------- Mirror of en.json so users with en_US locale don't 404 on the locale fetch (the file resolves on disk; whether Apache serves the path is env-specific). Translations from this commit are picked up regardless of the loadTranslations outcome above. Verified locally: - `npx eslint src/main.js`: clean - `npx webpack --mode production`: builds clean (3 unrelated warnings) - `node tests/validate-manifest.js`: PASS against published schema 1.2.0
Contributor
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 425/425 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Coverage: 0% (0/115 statements)
Quality workflow — 2026-05-09 13:48 UTC
Download the full PDF report from the workflow artifacts.
Pulls in the Vue.extend frozen-component fix (ConductionNL/nextcloud-vue#164, closes #161). With this version the manifest-driven shell mounts cleanly in the browser — the previous 1.0.0-beta.5 crashed at `Vue.extend` with "Cannot add property _Ctor, object is not extensible" because `pageTypes.js` and `CnPropertyValueCell` both routed dynamic-import component resolution through frozen namespace wrappers. The fix unwraps `m.default` before Vue receives the component. End-to-end verified at localhost:8080: - Decidesk shell mounts (CnAppRoot reads manifest) - Sidebar nav (CnAppNav) renders manifest.menu — Dashboard / Minutes / Decisions / Action items / Motions / Meetings / Documentation / Settings - Dashboard page renders 3 widget cards via CnDashboardPage async dispatch - Clicking Decisions navigates to /apps/decidesk/decisions and renders CnIndexPage (Cards/Table toggle, Add Item, Actions, sidebar Search + Columns tabs) - Vue Router resolves route components without _Ctor crash - i18n: Dutch labels render (Notulen ter goedkeuring, Gepubliceerde besluiten, Open actiepunten)
Contributor
Quality Report — ConductionNL/decidesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 425/425 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Coverage: 0% (0/115 statements)
Quality workflow — 2026-05-09 14:57 UTC
Download the full PDF report from the workflow artifacts.
This was referenced May 9, 2026
rubenvdlinde
added a commit
to ConductionNL/nextcloud-vue
that referenced
this pull request
May 13, 2026
* ci(docs): trigger full Docusaurus deploy on beta push (no main merge required)
Lets us ship the new unified docs site to nextcloud-vue.conduction.nl
straight from beta. Until now `documentation.yml` only fired on push to
main; the beta channel only refreshed /beta/styleguide/. With the
unification just landed on beta (PRs #135 -> #160), waiting for a
beta -> main release to make it visible at the public root would be a
ceremonial gate with no value — the site is the same content either way.
What changes:
documentation.yml
+ adds `beta` to push + pull_request triggers (was: main only)
+ adds `concurrency: { group: gh-pages-deploy }` so this workflow
serializes against the parallel beta/dev workflows that also
push to the gh-pages branch (avoids race conditions where two
`peaceiris/actions-gh-pages@v3` jobs try to push at once)
+ bumps Node 20 -> 24 to match `code-quality.yml` (PR #132)
+ adds `--legacy-peer-deps` to the styleguide install (the lib
ships Vue 2.7 alongside Vue 3 peer deps from `@nextcloud/files@4.x`)
documentation-beta.yml
+ same concurrency group
+ same Node 24 + --legacy-peer-deps fix
+ comment clarifying that this workflow is now redundant with
documentation.yml's beta trigger but kept for the
/beta/styleguide/ historical URL
documentation-dev.yml
+ same concurrency group
+ same Node 24 + --legacy-peer-deps fix
Effect on the gh-pages branch after this lands:
Push to beta:
documentation.yml -> gh-pages root + /styleguide/ (NEW)
documentation-beta.yml -> gh-pages /beta/styleguide/ (existing)
Push to main:
documentation.yml -> gh-pages root + /styleguide/ (unchanged)
Push to development:
documentation-dev.yml -> gh-pages /dev/styleguide/ (unchanged)
The very push of this commit to beta will trigger documentation.yml,
which deploys the new unified site at https://nextcloud-vue.conduction.nl/.
* fix(styleguide): add @nextcloud/notify_push so the styleguide build can resolve liveUpdates
The styleguide bundles `src/components/index.js` for its sandbox, which
transitively pulls in the live-updates plugin (`src/store/liveUpdates/`)
that was added recently. The plugin imports `@nextcloud/notify_push` —
that package is in the lib root `package.json` (line 63) but not in
`styleguide/package.json`, so the styleguide webpack build fails with:
Module not found: Can't resolve '@nextcloud/notify_push' in
'/home/runner/work/nextcloud-vue/nextcloud-vue/src/store/liveUpdates'
This blocks the production deploy because both `documentation.yml` and
`documentation-beta.yml` build the styleguide before pushing to
gh-pages. Same fix pattern as Phase 4 of the docs-unification project
(#157), which added `@codemirror/commands`, `apexcharts`, etc. as
explicit styleguide deps after the `--legacy-peer-deps` install
stopped hoisting them transitively.
The lib root pins `^1.0.0`; styleguide matches.
* fix(bundle): unfreeze component exports for Vue.extend consumers
Closes #161.
Symptom
-------
Consumers using `Vue.extend()` (directly, via Vue Router route
components, or via `<component :is>`) on imported library components
crash at runtime:
TypeError: Cannot add property _Ctor, object is not extensible
at Vue.extend
Reproduced end-to-end against decidesk (manifest-driven shell, PR
ConductionNL/decidesk#160) where the dashboard / index / detail pages
load via the manifest renderer's `defineAsyncComponent` dispatch.
Root cause
----------
Two paths in the lib's bundle output produced frozen namespace wrappers
(`Object.freeze({__proto__: null, default: <component>})`) that
`Vue.extend()` couldn't mutate:
1. `pageTypes.js` used `defineAsyncComponent(() => import('...vue'))`.
With rollup's `inlineDynamicImports: true`, every `import()` call
gets flattened into `Promise.resolve().then(() => namespace)` where
`namespace` is the frozen wrapper. Vue 2's async-component
resolution against that wrapper triggers the `_Ctor` crash on the
first downstream `Vue.extend()`.
2. `CnPropertyValueCell.vue` lazy-required `CnAdvancedFormDialog` to
break a circular dep — same dynamic-import shape, same frozen
wrapper, same crash.
Fix
---
Both paths now end the dynamic-import chain with `.then(m => m.default)`,
which extracts the component options object from the frozen wrapper
before Vue receives it. The unwrapped `m.default` IS extensible (it's
the raw `normalizeComponent(...)` output, which is mutated freely by
`rollup-plugin-vue`'s template injection step). Vue then attaches its
`_Ctor` cache without issue.
Also: convert all 61 `Cn*/index.js` files from
`export { default as Cn... } from './Cn....vue'`
to the explicit
`import Cn... from './Cn....vue'; export default Cn...; export { Cn... }`
shape used by the working `CnAppRoot` and `CnPageRenderer`. The
namespace re-export form was inviting rollup to emit additional frozen
`var Cn... = Object.freeze(...)` declarations as resolution targets;
the explicit shape is the proven-working pattern. No public API change
for consumers — every named export still resolves to the same component
options.
Verification
------------
- `npm test`: 646/646 passing.
- `npm run check:docs`: clean (110/110 exports documented).
- `npm run build`: clean (3 unrelated warnings).
- Browser smoke test (decidesk @ localhost:8080): full app shell
renders — sidebar nav reads manifest.menu, dashboard page renders
via `CnDashboardPage` async dispatch, no Vue.extend crashes.
The bundle still emits some `var Cn... = Object.freeze({__proto__: null,
default: ...})` declarations as side-effects of `inlineDynamicImports`
+ async-component dispatch — but they are now ALWAYS reached through
`.then(m => m.default)`, so no consumer-facing component is observed
frozen. A regression test asserting `Object.isExtensible` on every
exported `Cn*` is a worthwhile follow-up.
* fix(ci): scope concurrency group per-workflow + add workflow_dispatch
PR #162's shared `gh-pages-deploy` concurrency group across all three
documentation workflows turned out to be wrong. GitHub's rule is:
\"any previously pending job or workflow in the concurrency group will
be cancelled\". When a beta merge fires both a push event AND a
pull_request event for an open PR against beta, both queue in the same
group — and the older pending one (the push, which is what actually
deploys) gets cancelled by the newer pending one.
Result: every beta merge cancelled its own deploy. Manually re-running
the cancelled run made it cancel itself again on retry.
Fix:
- Scope the concurrency group per-workflow (`${{ github.workflow }}`)
so each workflow only serializes with itself. The parallel
documentation-beta.yml + documentation-dev.yml jobs may race the
gh-pages branch but `peaceiris/actions-gh-pages@v3` with
`keep_files: true` force-pushes — latest write wins, and writes go
to disjoint subdirs (`/`, `/beta/styleguide/`, `/dev/styleguide/`).
- Add `workflow_dispatch` to all three so deploys can be triggered
manually from the Actions UI or `gh workflow run` if a future event
sequence cancels another run.
Verified locally: `actionlint` passes (no new YAML / Actions schema
errors). After merge, the push event for THIS commit will trigger the
deploy and ship the new site.
* fix(styleguide): webpack alias for @nextcloud/notify_push (exports map)
Adding the package to styleguide/package.json (#163) was necessary but
not sufficient. @nextcloud/notify_push@1.x ships ONLY an "exports.import"
condition with no "main" or "module" field:
"exports": {
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }
}
Webpack 4 (used by vue-styleguidist@4) doesn't understand the "exports"
condition — it looks for "main" / "module", finds neither, and bails
with "Module not found: Can't resolve '@nextcloud/notify_push'". The
package IS in node_modules; webpack just can't see it.
Same workaround pattern the styleguide already uses for several other
modern packages (p-queue, devlop, unist-util-visit-parents/do-not-use-color):
add an explicit `resolve.alias` entry pointing at the dist file.
Verified locally: cd styleguide && npm ci --legacy-peer-deps && npm run build
finishes with "Style guide published to: ...styleguide/build".
* feat(docs): adopt @conduction/docusaurus-preset for brand-default chrome
Replaces the bare Docusaurus classic-theme template with the Conduction
brand preset (Cobalt tokens + Plex Mono + KvK/BTW footer + four-locale
i18n scaffolding + brand navbar/footer swizzles) used by conduction.nl
and procest. Mirrors procest's createConfig() pattern.
What changes:
docusaurus/package.json
+ adds @conduction/docusaurus-preset@^2.1.1 as a runtime dep
(deps: none — pure brand layer)
docusaurus/docusaurus.config.js
REWRITES around `createConfig({...})` from the preset. Library-
specific overrides preserved verbatim:
- en + nl locales only (preset default is nl/en/de/fr; we don't
ship de/fr translations yet)
- presets.classic.docs.path = '../docs' (the unification's
docs-source layout)
- presets.classic.docs.editUrl = function form pointing at
edit/beta/docs/<docPath> (avoids the .. relpath doubling
from string-form editUrl + path: '../docs')
- themeConfig.playground.styleguideUrl read by Playground.js
- themeConfig.prism + mermaid themes
- markdown.mermaid = true (post-createConfig assignment;
createConfig doesn't pass arbitrary top-level fields through)
- themes: [BRAND_THEME, '@docusaurus/theme-mermaid'] — re-include
the brand theme entry alongside mermaid because createConfig
replaces themes wholesale when `themes:` is passed
- Three footer columns: Docs (Getting Started / Components /
Architecture / Design Tokens), Related (Nextcloud-Vue
Components / Nextcloud Layout Components / Nextcloud App
Development), and the brand "Conduction" column spread from
baseFooterLinks() to preserve the corporate anchor
- copyright omitted -> brand KvK/BTW/IBAN string inherits
unchanged from the preset
Existing src/css/custom.css and src/pages/index.js are kept as-is.
Brand CSS (cobalt tokens, navbar/footer swizzles, hex artwork) is
auto-loaded by the BRAND_THEME entry; site-specific custom.css is
appended after.
Verified locally: cd docusaurus && npm install --legacy-peer-deps &&
npm run build succeeds for both en + nl. Three pre-existing broken-link
warnings on /privacy/, /terms/, /iso/ inherited from the brand footer
— non-fatal via onBrokenLinks: 'warn'; will resolve once those pages
exist on conduction.nl or get an explicit override.
Stack: lands on top of #166 (the deploy unblock series).
* fix(docs): empty custom.css so brand.css wins (preset adoption follow-up)
The preset's brand.css loads via getClientModules() in the brand theme
entry, but customCss is appended AFTER it. The 121-line legacy
custom.css from before the preset adoption was setting:
--ifm-color-primary: #2fb298 /* Open Webconcept green */
--ifm-font-family-base: 'Poppins'
...121 lines of dark-mode overrides...
— overriding the brand's cobalt + Plex Mono mappings on every variable
the preset cared about. Result: the live site shipped with the
preset's chrome (navbar swizzle, footer swizzle) but the wrong colors
and typography.
Replace with a near-empty placeholder that documents the load order
and leaves room for nextcloud-vue-specific tweaks. Verified: built
CSS now contains --c-blue-cobalt, --c-orange-knvb, the full cobalt-50
through cobalt-700 ramp; bundle size 71 KB -> 177 KB confirms the
brand tokens landed.
* fix(docs): clean up preset-adoption follow-up warnings
After PR #167 + #168 the build was clean for content but emitted three
classes of broken-link warnings. This PR closes them all:
1. /privacy /terms /iso (3 warnings × 2 locales)
Source: the preset's brand Footer swizzle hard-codes
<Link to=\"/privacy\"> etc. — those pages live on conduction.nl, not
on this docs site. Fix: stub each as a redirect page in
src/pages/{privacy,terms,iso}.jsx that <BrowserOnly>-redirects to
conduction.nl/<path> on first paint. Routes resolve at build time
(no warning), users still land on the right page.
2. ./layouts/ ./components/ ./design-tokens/ (3 warnings × 2 locales)
Source: getting-started.md used trailing-slash relative folder links
like ./layouts/. The preset defaults trailingSlash: true (was false
in our config); under true, ./layouts/ resolves from the current
page's path, not the parent dir, and 404s. Fix: switch those to
absolute /docs/<path>. Removed our explicit trailingSlash: false
override so the preset default carries.
3. /styleguide/ from components/index.md (1 warning × 2 locales)
Source: the :::tip admonition I added in Phase 5 used a markdown
link to /styleguide/, a path mounted by the gh-pages deploy and
thus invisible to Docusaurus's checker. Fix: rewrite the link as a
plain HTML <a> so the checker leaves it alone. Same destination.
Also drops the canal-footer mini-games (minigames: false in the
themeConfig) — same pattern as procest, which is also a docs site:
canal scenery + brand block stay, the boat-sinking + kade-cyclist
games go away.
Verified locally: rm -rf build && npm run build emits 0 warnings, 0
errors for both en and nl locales.
* chore(ci): pre-commit hook auto-regens docs/components/_generated/
Closes G3.5 of the auto-update guarantee from the unify-component-docs
spec (openspec/specs/component-reference/spec.md, Requirement:
Auto-regeneration on commit).
What lands:
scripts/precommit-regenerate-partials.sh
Pre-commit script. When the staged set contains a
`src/components/Cn*/Cn*.vue` file, it cd's into docusaurus/ and
runs `npm run prebuild:docs`, then `git add docs/components/_generated/`
so the SFC change + the regenerated partial commit atomically.
Skip-path: when no Cn* SFC is staged, the script exits 0 silently
(cost: one `git diff --cached --name-only | grep`). When
docusaurus/node_modules is missing (new contributor, first commit
before they've run `cd docusaurus && npm install`), the script
prints a one-line "install once with…" hint and exits 0 — never
blocks a commit. CI's freshness gate (G1) still catches the stale
partial at push time, with the same hint.
.husky/pre-commit
Thin entry that delegates to the script above. Husky chosen over
simple-git-hooks because git-submodule worktrees in this monorepo
have `.git` as a file pointer (not a directory); simple-git-hooks
writes to `.git/hooks/<name>` literally and fails with ENOTDIR,
while husky uses `core.hooksPath = .husky/_` and works regardless.
package.json
+ husky ^9.1.7 in devDependencies
+ "prepare": "husky || true" wires the hook in on `npm install`.
`|| true` swallows failures from environments without git
(npm pack tarballs, fresh CI checkouts before fetch).
CLAUDE.md
"Workflow when adding or modifying a component" updated to start
with `git commit` instead of `npm run prebuild:docs && git add` —
the hook does the regen for you. Manual fallback documented for
the "deps not installed yet" case.
Tested locally end-to-end:
Skip path: `git add scripts/some-other-file.js && bash .husky/pre-commit`
→ exits 0, no output, no _generated/ touched.
Happy path: edit `src/components/CnDataTable/CnDataTable.vue` (changed
a prop's JSDoc from "shows loading spinner" to
"shows loading spinner overlay")
`git add src/components/CnDataTable/CnDataTable.vue && bash .husky/pre-commit`
→ "[pre-commit] Cn* SFC staged — regenerating docs/components/_generated/"
`git diff --cached docs/components/_generated/CnDataTable.md`
→ exact one-word diff in the loading prop description, auto-staged.
* feat(manifest): cardComponent config on type:"index" for bespoke card-grid views
Adds an optional `cardComponent` field on `pages[].config` for `type:"index"`
that, when set + the page is in card-grid view mode, mounts the consumer-
provided component named in the `customComponents` registry for each row
in place of the schema-driven `CnObjectCard`. Closes the gap that forced
consumers needing a bespoke card layout (e.g. softwarecatalog Organisaties)
to fall back to `type:"custom"` and reimplement the entire index page.
Resolution priority (highest first):
1. `#card` scoped slot — App.vue overrides always win.
2. `cardComponent` registry entry — manifest-driven dispatch.
3. `CnObjectCard` — schema-driven library default.
Unknown registry names log `console.warn` once and fall back to the
default so a misconfigured manifest never blanks the grid.
The resolved component receives `{ item, object, schema, register, selected }`
props and emits `click` (forwarded as `row-click`) + `select` (forwarded
as `select`).
- Schema bumps to v1.3.0; v1.x manifests without `cardComponent` continue
validating (additive, backwards-compatible).
- 11 new component tests + 3 new schema-fixture tests; 660/660 tests pass.
- Docs updated in CnIndexPage.md, docs/components/cn-index-page.md, and
CnAppRoot.md (clarifies that `customComponents` now also resolves
index card components).
- Full openspec change at openspec/changes/manifest-card-index-component/.
Refs ADR-024 (app-manifest) v2 backlog row.
* feat(manifest): form page type for manifest-driven forms
Adds `type: "form"` to the manifest renderer. Closes the runtime-form
gap that today forces every consumer's public-form / runtime-survey
routes onto `type: "custom"`.
- New CnFormPage component (renders fields + submit, dispatches via
registered handler OR HTTP endpoint with :param substitution).
- New cnRenderFormField helper (boolean / number / string / password
/ enum / json + textarea widget hint).
- Schema description + validateManifest 'form' branch (fields[]
required, exactly-one of submitHandler|submitEndpoint, method/mode
closed enums).
- 24 new tests across CnFormPage, cnRenderFormField, schema validator,
and CnPageRenderer dispatch (full suite 39/39, 682/682 passing).
- New cn-form-page.md doc + migrating-to-manifest.md updates.
Pipelinq's PublicSurvey route migrates from custom→form in the
companion consumer PR; the other six pipelinq form-builder routes
stay custom because they are authoring UIs (drag-drop questions,
analytics) that the manifest's declarative shape can't represent.
OpenSpec: openspec/changes/manifest-form-page-type/
* docs(openspec): add manifest-actions-dispatch change (proposal/design/tasks/spec)
Proposes a `handler` field on the `action` $def so manifest-declared
row actions in `pages[].config.actions[]` dispatch to consumer-provided
functions in the customComponents registry. Three reserved keywords
(navigate / emit / none) cover the no-function-needed cases.
Closes the lib v2 backlog row from ADR-024 — actions[] dispatch — that
keeps 11 opencatalogi routes (PR #547) and 2 pipelinq queue routes
(#330 just merged) on type:"custom" today.
Spec includes REQ-MAD-1..8 covering schema bump (1.2.0 → 1.3.0),
validator changes, CnIndexPage runtime resolution, navigate/emit/none
keywords, function-handler back-compat, and CnDetailPage delegation.
* feat(manifest): action $def adds handler + route fields, schema bumps to 1.3.0
REQ-MAD-1 / REQ-MAD-2 — manifest-actions-dispatch.
Adds two fields to the `action` $def in app-manifest.schema.json:
- `handler` (string, pattern `^(navigate|emit|none|[A-Za-z][A-Za-z0-9_]*)$`):
registry name resolving to a function in the customComponents
registry, OR one of three reserved keywords (`navigate` / `emit` /
`none`).
- `route` (string): required when handler === "navigate"; ignored
otherwise. Used as the vue-router push target.
Schema's `version` field bumps from 1.2.0 to 1.3.0.
Extends `validateActionsArray()` in validateManifest.js to:
- type-check handler as a string when present,
- reject pattern violations (hyphens / dots / spaces),
- require `route` when handler === "navigate".
Test additions:
- 8 new validator scenarios in app-manifest-refs.spec.js (good-shape,
navigate+route, navigate-missing-route, hyphen-rejected,
keyword-acceptance, non-string-rejected, v1.2-back-compat).
- schema-version assertions in -defs / -refs / -schema spec files
bump from 1.2.0 to 1.3.0.
All 232 schema/validator tests pass.
* feat(CnIndexPage): resolve actions[].handler through customComponents registry
REQ-MAD-3..7 — manifest-actions-dispatch.
Wires the manifest-declared `actions[].handler` field into the runtime:
- `inject: cnCustomComponents` (provided by CnAppRoot today). Fall
through to an empty object so the page still mounts standalone.
- `customComponents` prop for explicit per-instance override. Prop wins
over inject (mirrors CnPageRenderer's pattern).
- New `effectiveCustomComponents` computed combines prop + inject.
- New `resolveHandler(action)` method maps action.handler:
* function (back-compat / programmatic) → return as-is.
* "navigate" → returns a closure that calls
`$router.push({ name: action.route, params: { id: row[rowKey] } })`;
warns + nulls when `route` is missing.
* "emit" → null (page still emits @action).
* "none" → returns a no-op function AND flags the action so
onRowAction drops the bubbled @action emit.
* registry name → looked up in effectiveCustomComponents; function
entry wraps as `(row) => fn({ actionId, item: row })`. A
non-function entry warns once and falls back to @action-only.
* unknown name → silent fall-back to @action-only.
- `mergedActions` computed now resolves manifest-declared string
handlers via resolveHandler, so CnRowActions / CnContextMenu see
the same `{ handler: fn }` shape they already consume.
- New `onRowAction` method intercepts CnRowActions @action emits to
honour the "none" keyword's emit-suppression flag.
11 dispatch-specific tests in CnIndexPageActionsDispatch.spec.js
cover every requirement scenario (registry resolution, prop-vs-inject
precedence, missing-name silent fallback, non-function warn,
navigate-route-pushes, navigate-missing-route-warns, emit-collision-
keyword-wins, emit-still-bubbles, none-suppresses-emit, function-
handler-back-compat, no-handler-back-compat).
All 664 tests pass (was 646 before this change, +11 dispatch + +8 validator).
* docs(CnIndexPage): document actions[].handler dispatch + customComponents prop
Adds the new `customComponents` prop row to both the Docusaurus prop
table (docs/components/cn-index-page.md) and the SFC-side props
table (src/components/CnIndexPage/CnIndexPage.md). Both pass
`npm run check:docs`.
Adds an "Action handlers (manifest-actions-dispatch)" section to
cn-index-page.md covering:
- registry-name handler example (manifest + customComponents.js
registry entry), showing `{ actionId, item }` is the call shape.
- the three reserved keywords (`navigate` / `emit` / `none`) with
minimal manifest examples.
- fallback semantics — missing name = silent, non-function entry
= warn-and-fall-through, programmatic function handlers stay
untouched.
Updates `actions` prop description to reference the new dispatch
path so consumers reading the existing prop table land on the right
section.
* feat(manifest): chart widget primitive (line/bar/donut/area) via apexcharts
Wire `type: "chart"` into CnDashboardPage's widget dispatcher so
manifest-driven dashboards can declare apexcharts visualisations
without consumer-side component code. Closes the lib v2 backlog row
"chart widget primitive (apexcharts) — procest Doorlooptijd,
opencatalogi Dashboard" cited in ADR-024 §10.
Dispatcher priority order is tile → custom slot → chart → NC API →
unknown fallback. Custom slots still beat chart widgets so apps with
apexcharts annotations / events outside the manifest contract keep
the slot escape hatch.
Widget API rides `widgetDef.props`:
{ chartKind: 'line'|'bar'|'donut'|'area'|'pie'|'radialBar',
series, categories, labels, options, colors, toolbar, legend,
height, width, dataSource? }
`chartKind` is renamed to apexcharts' `type` inside the dispatcher
to avoid colliding with `widgetDef.type`'s dispatcher meaning. The
`dataSource` union (`{ url } | { register, schema, groupBy,
aggregate }`) is round-tripped through manifest validators today;
the resolver lands in a follow-up cycle.
- 8 new dispatcher tests (CnDashboardPage.spec.js)
- Spec delta in openspec/changes/manifest-chart-widget/ (proposal,
design, tasks, dashboard-page spec.md)
- JSDoc + manifest example added to CnDashboardPage SFC + docs
- Manifest-usage section added to CnChartWidget docs
- Regenerated docs/components/_generated/CnDashboardPage.md
* docs: regenerate auto-doc partials (CI: 'Auto-doc partials are fresh' check)
* docs: regenerate auto-doc partials (CI: 'Auto-doc partials are fresh' check)
* docs: regenerate auto-doc partials (CI: 'Auto-doc partials are fresh' check)
* fix(jsdoc): land CnFormPage in baseline at 89% + add inline event/slot docs
Lib v2 PR #172 introduced CnFormPage; CI's JSDoc completeness ratchet
demands 100% from new components. Added @event JSDoc to each
`this.$emit` site (input/submit/error) and HTML-comment descriptions
above each named slot (header/actions/field-${field.key}/submit). Two
slots (actions/submit) didn't get picked up by vue-docgen-api despite
the same comment pattern other components use successfully — admitted
in the baseline at 89% and recorded as the floor for future PRs to
ratchet up.
* feat(docs): widget-card homepage + four hub pages
Implements the openregister.conduction.nl-style landing for
nextcloud-vue: a brand <Hero/> on top, then a single <Section/> with
four <Card/>s each linking to one conceptual layer of the library.
Same shape used on conduction.nl + procest.
Three new hub pages:
docs/architecture/app-design-principles.md
The chassis (topbar, left nav, main column), the five atoms
(topbar / left navigation / main column / page header / sidebar),
and the stacked views the library composes on top
(CnIndexPage / CnDetailPage / CnDashboardPage / CnSettingsPage).
Uses three <AppMock/> visuals from the preset (openregister,
procest, mydash) to show that three different apps share one
chassis.
docs/architecture/manifest.md
Overview of the JSON manifest pattern: what it does (declarative
pages + nav + dependencies), why JSON not Vue (inspectable,
remotable, survives lib upgrades), the page-type table
(index / detail / dashboard / settings / logs / chat / files /
custom), and how it configures the five atoms (headerComponent,
actionsComponent, sidebar, slots). Links to migrating-to-manifest
for the step-by-step guide.
docs/architecture/schemas-and-registers.md
Two-consumers-one-schema explainer: the same JSON Schema drives
OpenRegister's typed records AND the @conduction/nextcloud-vue
frontend (columns / filters / forms / facets / search / admin-
only). Walks through columnsFromSchema / filtersFromSchema /
fieldsFromSchema. Explains registers as deployment-time scoping.
Lists which schema flags activate which integration tabs in
CnObjectSidebar (audit / files / notes / tasks / tags / chat).
Homepage rewrite (docusaurus/src/pages/index.js):
Replaces the bare hero+three-feature template with the brand
preset's <Hero/> + <Section/> + <SectionHead/> + four <Card/>s.
Each card has an eyebrow ("01 · The chassis", etc.), a one-
sentence title, a 2-3-line body, and a "Pick this →" CTA — same
rhythm conduction.nl uses.
Removed src/pages/index.module.css — no longer imported (the brand
preset provides all the chrome).
Verified locally: npm run build succeeds for both en + nl (two
non-fatal pre-existing warnings: preset's optional app-downloads.json
data loader and vscode-languageserver-types module-resolution).
* fix(styleguide): unblock build (buble + bespoke-card example)
Two issues stopped the styleguide build for every commit since #171
(card-index merge, ~10 minutes before #176 was opened).
1. Buble's `moduleExport` transform isn't implemented in the version
pulled into styleguide/node_modules/. Every component example with
<script>export default {...}</script> (which is most of them since
day one) failed with:
CompileError: Transforming export is not implemented.
Use `transforms: { moduleExport: false }` to skip transformation
and disable this error. (3:0)
Fix: add `moduleExport: false` to compilerConfig.transforms,
exactly as the error message suggests. Webpack picks the export
keyword up downstream — buble doesn't need to translate it.
2. CnIndexPage.md (added in #171) showed a manifest pattern for
bespoke card components. Two of the snippets — a `js` block
declaring `import OrganisatieCard from './components/cards/...'`,
and a `vue` block referencing the registered component — are
*consumer-side* code, not runnable inside the styleguide sandbox.
The styleguide tried to live-render them anyway and tripped on the
missing module path.
Fix: tag both fences with vue-styleguidist's `{static}` modifier
so they're rendered as syntax-highlighted documentation, not
evaluated as live demos. Same pattern used by other docs files
that show consumer-side wiring.
Verified locally: `cd styleguide && npm install --legacy-peer-deps &&
npm run build` -> "Style guide published to: ...styleguide/build".
After merge, the documentation.yml deploy that's been failing since
#171 should land cleanly and ship the new homepage + four hub pages
from #176.
* fix: dedupe effectiveCustomComponents() — merged actions-dispatch + card-index definitions
* fix: resolve test conflict markers + reconcile schema version on 1.3.0 (card-index + actions-dispatch)
* fix: resolve remaining 4 conflict markers in app-manifest.schema.spec.js
* feat(manifest): dynamic per-tenant menu entries via backend merge (#178)
Canonicalises the existing useAppManifest backend-merge path as the
mechanism for dynamic per-tenant menu entries (ADR-024 lib v2 backlog
rows: "Dynamic per-tenant menu entries" + "Backend /api/manifest endpoint
implementation").
Picks Option A from openspec/changes/manifest-dynamic-menu/design.md:
backend populates menu[] wholesale, useAppManifest's array-replace
deep-merge absorbs it. No new schema fields, no new lib runtime code.
The rejected Option B (dynamicSource on menuItem) would have bled the
data layer into the manifest contract, violating ADR-022.
Lib changes:
- openspec/changes/manifest-dynamic-menu/{proposal,design,tasks}.md
+ specs/manifest-dynamic-menu/spec.md (5 ADDED requirements with
Gherkin scenarios)
- 5 new fixture tests in tests/composables/useAppManifest.spec.js
locking the contract: children-form fan-out, flat-list replacement,
404 fallback, malformed menu fallback, dynamicSource pass-through
- docs/utilities/composables/use-app-manifest.md: new "Dynamic
per-tenant menu entries" section with worked opencatalogi-style
catalogue example
- docs/migrating-to-manifest.md: new subsection in Tier 3 pointing at
the contract
- docs/components/cn-app-nav.md: cross-reference to the dynamic-menu
pattern
Tests: 673/673 passing (10 baseline + 5 new in useAppManifest).
Gates: check:docs green (111 exports + 70 components covered),
check:jsdoc green (67 components at baseline).
* fix(CnPageRenderer): merge $route.params into props forwarded to child (#180)
CnPageRenderer's resolvedProps was returning only the manifest's static
page.config — never merging $route.params. Vue Router's `props: true`
on the generated route binds params to the routed component, which is
CnPageRenderer itself, not the dispatched child page component. So a
dynamic-route child like decidesk's LiveMeeting (route /meetings/:id/live,
prop `id: { type: String, required: true }`) received `id: undefined`
even though the URL clearly contained the UUID. Worse, fetchObject(type,
undefined) silently builds the collection URL via _buildUrl's `if (id)`
guard, returns the paginated wrapper, and the calling component never
sees its single object.
Merge $route.params with config (params winning collisions so URL truth
trumps manifest defaults). Children that declare route-derived props
now receive them, fetchObject(type, id) builds the right URL, and the
LiveMeeting demo's two-browser e2e becomes possible.
722/722 tests pass.
* feat(CnSettingsPage): multi-tab settings + component widget body kind (#173)
* feat(CnSettingsPage): multi-tab settings + component widget body kind
Adds two strictly additive extensions to the type:"settings" page surface
(manifest-settings-orchestration spec, REQ-MSO-1..8):
1. Multi-tab orchestration — pages[].config MAY declare
tabs: [{ id, label, sections }] in PLACE OF (XOR) the flat
sections[]. CnSettingsPage renders a tab strip and routes each
tab's sections through the same dispatcher used by the flat case.
Tab IDs MUST be unique; first tab is active by default; the new
`initialTab` prop overrides; @tab-change emits on switch.
2. {type:"component", componentName} widget discriminator — explicit
way to host a consumer Vue component as a widget body. Resolves
`componentName` against customComponents. The legacy
widget.type → customComponents fallback path is kept for
back-compat (REQ-MSRS-2 consumers) but JSDoc-deprecated.
Schema, validator, component, and 32 new tests (14 component +
17 schema + 1 fixture). `npm test` 678/678 green; `check:docs` green;
no schema version bump (still 1.2.0). Existing flat-fields and
rich-sections manifests render and validate identically.
* docs: regenerate auto-doc partials (CI: 'Auto-doc partials are fresh' check)
* test(jest): mock gridstack via moduleNameMapper to fix CnDashboardPage suite
The real gridstack ships ESM that crashes Jest's parser before per-test
`jest.mock('gridstack', ..., { virtual: true })` can intercept. Move
the stub to a moduleNameMapper-resolved file alongside the other
@nextcloud / vueuse mocks; CnDashboardPage suite now passes (8 / 8)
and the full 40-suite / 694-test set is green.
Pre-existing failure on beta — fixed here so settings-orchestration
and the next CI runs unblock.
* feat(manifest): @resolve: sentinel loader for tenant-configured slugs (#179)
* feat(manifest): add resolveManifestSentinels loader + wire into useAppManifest
Implements the loader half of the manifest-resolve-sentinel capability.
Walks pages[].config subtrees and substitutes every fully-matched
@resolve:<key> string with the consuming app's IAppConfig value.
Resolution chain: @nextcloud/initial-state slot first (zero-network),
runtime fetch second (per-(appId, key) cached), null third. Unset keys
substitute null and accumulate on a returned unresolved[] list so
useAppManifest can surface them via a new unresolvedSentinels ref.
* feat(manifest): reject @resolve: sentinels in router/registry paths
Build-time validation (when the validator runs against a static manifest
that has not been through the loader) MUST permissively accept the
sentinel under pages[].config.* and explicitly reject it in router /
registry-key paths: version, dependencies[], menu[].id, menu[].route,
pages[].id, pages[].route, pages[].component, pages[].headerComponent,
pages[].actionsComponent, and pages[].slots.*. Each rejection cites the
offending JSON path so consumers know exactly which field to fix.
The runtime loader never feeds an unresolved sentinel to the validator
(substitution runs first), so this guard only fires at build-time
manifest checks where the sentinel is still raw.
* test(manifest): cover @resolve: resolver, integration, and validator rules
- tests/utils/resolveManifestSentinels.spec.js (13 cases) — pass-through,
replacement, unset-key warn, undefined / empty treated as unset, nested
config substitution, sentinels outside pages[].config left intact,
partial sentinels ignored, malformed sentinels (bad chars / no key)
ignored, shared keys deduped to a single fetch, mixed resolved /
unresolved tally, input not mutated, arrays under config, null input.
- tests/composables/useAppManifest.spec.js (4 new integration cases) —
resolver runs before validation; unresolvedSentinels ref populated for
unset keys; softwarecatalog-style fixture with @resolve:voorzieningen_register;
shared keys cause a single getAppConfigValue call.
- tests/schemas/app-manifest.schema.spec.js (8 new validator cases) —
sentinel accepted under pages[].config.* (top-level and nested);
rejected in route / id / version / dependencies / menu.route / menu.id /
pages[].component.
694/694 tests pass.
* docs(manifest): add @resolve: sentinel guide + bump useAppManifest section
- docs/utilities/resolve-manifest-sentinels.md — full reference for the
utility (sentinel grammar, allowed locations, resolution chain, empty-
state semantics, standalone + test-fixture usage).
- docs/utilities/clear-resolve-cache.md — test-only cache reset helper.
- docs/utilities/composables/use-app-manifest.md — promote three-phase
flow to four-phase (sentinel resolution between merge and validation),
document the new unresolvedSentinels ref + getAppConfigValue option,
add a dedicated "@resolve:<key> sentinel" section.
- docs/migrating-to-manifest.md — new "Per-tenant config slugs" section
with a softwarecatalog-style example and rule-of-thumb guidance.
- docs/utilities/index.md — surface the new Manifest helpers in the
utility index table.
- openspec/changes/manifest-resolve-sentinel/tasks.md — mark
implementation, spec, and docs phases complete (Phases 1–4); leave
cross-app coordination Phases 5–6 unchecked since they are tracked
per-app, not in this PR.
- openspec/changes/add-json-manifest-renderer/.../spec.md — cross-link
the resolve-sentinel change under "Subsequent extensions".
* fix(lint): suppress n/no-extraneous-require for optional initial-state peer
The @nextcloud/initial-state package is documented as an optional peer
provisioned by the host page (see surrounding comment block); add the
rule to the existing eslint-disable so Frontend Quality stops failing
on what is by design a dynamic, may-not-exist require.
* test(jest): mock gridstack via moduleNameMapper to fix CnDashboardPage suite
The real gridstack ships ESM that crashes Jest's parser before per-test
`jest.mock('gridstack', ..., { virtual: true })` can intercept. Move
the stub to a moduleNameMapper-resolved file alongside the other
@nextcloud / vueuse mocks; CnDashboardPage suite now passes (8 / 8)
and the full 40-suite / 694-test set is green.
Pre-existing failure on beta — fixed here so settings-orchestration
and the next CI runs unblock.
* ci: trigger workflow re-run after gridstack mock cherry-pick
* feat(manifest): wiki page type (#181)
* feat(manifest): wiki page type for manifest-driven wiki pages
Adds a new `type: "wiki"` page type that renders a markdown article
plus an optional sidebar tree, dispatched by CnPageRenderer for any
manifest-declared wiki/help/docs route.
Covers REQs in openspec/changes/manifest-wiki-page-type/:
- Schema + validateManifest accept `type: "wiki"` with required
register/schema and optional sidebarSchema/sidebarRegister/
contentField/titleField/idParam/treeField/sidebarTitleField
- New CnWikiPage component with #header/#body/#sidebar/#empty slots
and @tree-click/@error events
- New cnRenderMarkdown composable wraps marked.parse with
{ gfm: true, breaks: false }
- CnPageRenderer.defaultPageTypes registers wiki via
defineAsyncComponent
- 100% JSDoc on the new component, full doc page +
migrating-to-manifest update
WIP: pre-merge with origin/beta — see follow-up merge commit.
* fix(CnWikiPage): replace @example tags + regen docgen partials
vue-docgen-cli mistakenly treated the docblock's `@example` tag
content as a path-to-load (multiMd.js spent the example text as a
filename). Drop the JSDoc `@example` markers in favour of plain
"Basic usage:" / "With sidebar tree:" prose with the same code
samples. Other CnPage components don't use `@example` either, so
this matches existing convention.
Side effect: `docusaurus prebuild:docs` now regenerates
docs/components/_generated/CnWikiPage.md and
docs/components/_generated/CnWikiTreeNode.md — committed alongside
since the prebuild step is also a CI freshness check.
* feat(manifest): named-view sidebar (#182)
Adds `pages[].sidebarComponent` — an optional registry-name field on
the manifest's `pages[]` entry — for the per-route full-sidebar swap
pattern (Vue Router named-view equivalent). Resolves against the
consuming app's `customComponents` registry; CnPageRenderer publishes
the resolved component on a new `cnPageSidebarComponent` reactive
provide channel; CnAppRoot mounts it as the DEFAULT content of its
`#sidebar` slot. Composes with the existing `sidebar.show` gate
(visibility wins; warning logged on dead config).
Implements REQ-MNVS-1..5:
- REQ-MNVS-1 schema: `pages[].sidebarComponent` is an optional
non-empty string; documented in the JSON schema with
precedence-with-`sidebar.show` rules.
- REQ-MNVS-2 renderer: `CnPageRenderer` adds
`currentPageSidebarComponent` computed + `pageSidebarComponent`
reactive holder + provide channel; warns on missing registry name.
- REQ-MNVS-3 host shell: `CnAppRoot` injects
`cnPageSidebarComponent` (default `{ value: null }`) and renders
the resolved component as the slot's default content; consumer
`#sidebar` override still wins.
- REQ-MNVS-4 precedence: visibility wins; CnPageRenderer logs a
console.warn when both `sidebar.show: false` and
`sidebarComponent` are set on the same page.
- REQ-MNVS-5 backwards-compat: every addition is optional with a
non-breaking default; manifests without the field validate
unchanged; apps without a CnPageRenderer ancestor inject the
default holder and behave as before.
Validator: `pages[].sidebarComponent must be a non-empty string`
when present (free-form; registry-membership is a runtime concern).
Tests: new tests/components/CnPageRendererNamedViewSidebar.spec.js
covers the provide channel + precedence; CnAppRoot.spec.js gains a
`cnPageSidebarComponent inject (REQ-MNVS-3)` block; schema spec
covers valid/invalid `sidebarComponent` shapes.
Docs: cn-page-renderer.md gets the page-fields row + a "Per-page
sidebar component" section; cn-app-root.md notes the slot-default
behaviour; migrating-to-manifest.md adds a "Per-route sidebar swap"
section using opencatalogi's Search/SearchSideBar as the example.
OpenSpec change: openspec/changes/manifest-named-view-sidebar/
(proposal + design + tasks + spec).
Refs: Hydra ADR-024 (App manifest, fleet-wide adoption); lib v2
backlog row "Named-view sidebar (router-named-view pattern) —
opencatalogi Search".
* fix(docs): cobalt-blue hero + clickable widget cards on the homepage
Two homepage bugs:
1. Hero rendered light (white bg, dark text) instead of cobalt blue.
The preset's <Hero/> uses var(--c-cobalt-900) for text on whatever
the section bg is — there's no `inverse` variant. Switched to a
small custom hero block that sets background: var(--c-blue-cobalt)
directly and overrides text colours to white / cobalt-100 /
cobalt-200, matching the openregister.conduction.nl + procest
landing pattern. CTAs still use the preset's <Button/> primitive
so they pick up the brand button styling.
2. Widget cards weren't clickable. The preset's <Card/> only renders
as <a> when an `href` prop is set; passing `to` (the Docusaurus
internal-route prop) falls through to a non-clickable <div>. The
`to` value lands on the div as an unused HTML attribute. Switched
from `to={...}` to `href={...}` so the card emits an anchor; the
trade-off is full navigation instead of SPA routing on click,
which is fine on the homepage. URLs include trailing slashes to
match the preset's trailingSlash: true default and skip the
gh-pages 301 hop.
Verified locally: cd docusaurus && npm run build succeeds for both
en + nl. One pre-existing broken-link warning (resolve-manifest-
sentinels page from #179) unrelated to this PR.
* fix(styleguide): alias marked to mocks/empty.js (webpack 4 can't parse marked@15)
#181 added the wiki page type and pulled marked@^15.0.0 into the lib
root deps for the cnRenderMarkdown composable. The styleguide bundle
transitively imports cnRenderMarkdown via src/composables/index.js, so
it needs marked too — but marked@15 uses optional-chaining
(`cells.at(-1)?.trim()`) which the styleguide's webpack 4 parser
doesn't recognise (node_modules isn't run through babel-loader in
this config), failing the build with:
./node_modules/marked/lib/marked.umd.js 338:40
Module parse failed: Unexpected token (338:40)
Markdown rendering only matters inside CnWikiPage at runtime in a
real Nextcloud server. The styleguide demo just needs the component
to mount, so a no-op stub keeps the build green. Same alias pattern
used for @nextcloud/notify_push, p-queue, devlop — all of them shipped
ESM features webpack 4 can't grok.
Also adds marked to styleguide/package.json so npm ci finds something
to resolve before the alias kicks in (kept consistent with the other
peer-dep entries even though the alias means the actual marked code
never gets bundled). Sites consuming the lib pull marked from the
lib root's package.json normally.
Verified: cd styleguide && npm install --legacy-peer-deps && npm run
build -> 'Style guide published to: ...styleguide/build'.
* feat(manifest): map widget primitive (Leaflet + WMS/WFS/GeoJSON) (#184)
Adds a `type: "map"` page primitive to CnPageRenderer's defaultPageTypes
registry so manifest-driven apps can render a Leaflet map with WMS,
WFS, tile, and GeoJSON overlays without any consumer-side component
code. Implements the manifest-map-widget openspec change.
REQ-MMW-1 — `CnMapWidget` Leaflet wrapper consuming `{ center, zoom,
layers, markers, clustering, height, autoFit }` directly. Layer
dispatch picks `L.tileLayer | L.tileLayer.wms | L.geoJSON` per
`layer.type ∈ {tile, wms, wfs, geojson}`; unknown types log warn and
skip. `leaflet.markercluster` lazy-loads only when `clustering: true`.
REQ-MMW-2 — `CnMapPage` page-shell wrapping `CnPageHeader` + the
widget; forwards `map-ready / marker-click / bounds-change / click`
events and exposes `header / filters / legend / popup / fallback`
slots.
REQ-MMW-3 — `defaultPageTypes` gains a `map` entry (async-imported).
REQ-MMW-4 — schema description on `pages[].type` and `pages[].config`
enumerate `map` and document the per-type config shape.
REQ-MMW-5 — `validateManifest` adds a `case 'map':` branch enforcing
`center` length-2 finite-number array, optional finite `zoom`,
closed-enum `layers[].type`, non-empty `url` (or inline `data` for
geojson), and `markers.dataSource` exactly-one-of `{url}` |
`{register, schema}`. Reserved register-schema resolver deferred.
REQ-MMW-6 — `leaflet ^1.9.0` added as direct dependency (procest is
the only fleet consumer today; peer-dep would force install on every
consumer that doesn't render a map).
Component tests cover layer dispatch, marker rendering from both inline
features and dataSource.url, clustering opt-in lazy-load, event
emission, and the Leaflet-unavailable fallback. CnPageRenderer test
verifies `type=map` resolves to CnMapPage; schema tests cover valid
and invalid config shapes. JSDoc baselines bumped: CnMapPage and
CnMapWidget at 100% public-surface coverage.
Out of scope (deferred to follow-ups): drawing/spatial-filter UI,
register+schema marker resolver, RD<->WGS84 reprojection.
* feat(composables): useGraphQL + useDataSource for manifest dataSource resolution
useGraphQL is a thin reactive wrapper around axios.post against OR's
GraphQL endpoint. selectByPath reads dot-paths with [] flat-maps for
the manifest selectors map.
useDataSource resolves a manifest 'dataSource' block into reactive
{ data, loading, error }. Two forms supported: shorthand
{ register, schema, filter, aggregate: 'count' } that builds a
{ <slug>(filter:...) { totalCount } } query, and raw graphql
{ query, variables, selectors } for richer aggregates.
CnStatsBlockWidget is a thin wrapper around CnStatsBlock that
consumes useDataSource and forwards the resolved count. Mounted by
the type='stats-block' dispatcher in CnDashboardPage (next commit).
* docs: render AtomZones five-atoms visual on app-design-principles
Replaces the markdown atom table on /docs/architecture/app-design-principles/
with the new <AtomZones/> component shipped in @conduction/docusaurus-preset
2.5.0+. The component renders five zone cards — Topbar, Left navigation,
Main column, Page header, Sidebar — each showing the full chassis with the
focused atom outlined in KNVB orange and the rest dimmed to 25%.
This is the same "Five atoms, one chassis" reference visual the design-system
kit serves at preview/components/app-mock.html#five-atoms; reusing it via the
preset keeps every Docusaurus consumer in lockstep with the kit instead of
duplicating ~700 lines of inline HTML+CSS.
The library-component mapping (Topbar -> NcAppHeader, Left navigation -> CnAppNav,
Main column -> stacked views, Page header -> CnPageHeader, Sidebar -> CnIndexSidebar
or CnObjectSidebar) survives as a follow-up table so the deeplinks into the
component reference don't disappear.
* feat(dashboard): manifest-driven dataSource for stats-block + chart widgets
CnDashboardPage gets a stats-block dispatcher branch between chart
and NC API. Mounts the new CnStatsBlockWidget (committed earlier)
which forwards a GraphQL-resolved count from widgetDef.dataSource
into CnStatsBlock.
CnChartWidget gains an optional dataSource prop. When set, series /
categories / labels resolve from the GraphQL response via the
selectors map; static props remain the fallback while the request
is pending or when no dataSource is configured.
Manifest schema's widgetDef gets a dataSource sub-schema with two
forms: shorthand { register, schema, filter, aggregate: 'count' }
and raw { graphql: { query, variables, selectors } }.
Public barrels re-export the new composables (useGraphQL,
useDataSource, selectByPath, buildCountQuery) and the new
component (CnStatsBlockWidget) so consumer apps can adopt the
manifest pattern without lib internals.
Tests: 16 new across useGraphQL.spec.js + useDataSource.spec.js;
full suite at 871/871 (one pre-existing leaflet-missing test
suite failure is unrelated). Docs coverage check green.
* fix(styleguide): add leaflet + leaflet.markercluster as direct deps
The styleguide build was failing on beta after PR #184 introduced
CnMapWidget — vue-styleguidist + Webpack 4 doesn't reach into the
package's runtime peer deps, so leaflet and leaflet.markercluster
have to be listed in styleguide/package.json directly the same way
apexcharts/codemirror/marked already are.
Versions match the parent package.json: leaflet ^1.9.0 (current
1.9.4) and leaflet.markercluster ^1.5.3 (current 1.5.3). The
component lazy-imports both, so this is purely a build-time
resolver fix; no consumer code has to change.
Unblocks the gh-pages deploy of PR #187 (AtomZones).
* feat(index-page): hoist CnIndexSidebar through CnAppRoot
CnIndexPage's auto-mounted CnIndexSidebar (the index-page filter +
columns + facets panel) was rendered inline inside the cn-index-page
wrapper, which sits inside NcAppContent. NcAppSidebar must be a
DIRECT child of NcContent to render as the proper right-side
overlay; nested deeper it falls back to in-flow layout (the wide
left column users were seeing on /apps/decidesk/meetings).
CnAppRoot now provides a reactive 'cnIndexSidebarConfig' holder plus
a 'cnHostsIndexSidebar: true' sentinel via provide/inject. CnIndexPage
detects the host on mount, publishes its sidebar config (component +
props + listeners) into the holder, and skips the inline render. The
hoisted component mounts at NcContent level — sibling of the
consumer's '#sidebar' slot, so existing slot content
(e.g. CnObjectSidebar for detail pages) keeps working unchanged.
Apps mounting CnIndexPage standalone (without CnAppRoot) still see
the legacy inline rendering: the inject defaults to a no-op holder
and 'cnHostsIndexSidebar' defaults to false. No consumer changes
required.
Tests: 8 new in CnIndexPageSidebarHoist.spec.js cover both paths
(hoist active vs legacy inline) plus reactive prop updates and
listener re-emission. Full suite at 863/863.
* chore(docusaurus): bump @conduction/docusaurus-preset to 2.6.1
Pulls in the slimmer AtomZones (title + applies + description +
illustration only); the per-zone Inside/Tokens/Width/Height dl and
do/don't tips are dropped. The implementation-detail metadata still
lives on the design-system kit page; the architecture intro stays
focused on the chassis story.
* spec(collaborative-editing-defaults): proposal + tasks + sub-specs
Proposes auto-subscribe-on-detail-view + lock-on-edit as default
behaviour for CnDetailPage / CnObjectSidebar.
ADDED requirements:
- REQ-CO-LOCK-001..004 (composables): useObjectSubscription +
useObjectLock lifecycle, reactive lock state from store cache,
acquire/release + typed errors, auto-release on unmount + beacon
on beforeunload.
- REQ-DPG-COLLAB-001..004 (detail-page-grid): default subscribe +
default lock-on-edit + manifest opt-out (subscribe: false / lock:
false) + save/cancel/navigation release.
Implementation, tests, and docs follow in subsequent commits per
tasks.md.
* feat(detail): auto-subscribe + lock-state composables + locked banner
Implements the v1 deliverable from the collaborative-editing-defaults
spec.
Composables:
- useObjectSubscription(store, type, id, options) — wraps
objectStore.subscribe with a Vue scope-bound lifecycle (mount /
unmount / reactive id flips). No global state; idempotent.
- useObjectLock(store, register, schema, id, options) — derives
reactive lock state from the store cache (kept fresh by
liveUpdatesPlugin) and exposes acquire/release that hit OR's
/api/objects/{r}/{s}/{id}/lock endpoint. Exports typed
LockConflictError and PermissionError.
Component:
- CnLockedBanner — presentation-only locked-by-X notice strip.
Auto-wiring:
- CnDetailPage gains subscribe + objectStore props (default true /
null) and renders CnLockedBanner when @self.locked indicates a
remote user holds the lock.
- CnObjectSidebar mirrors the auto-subscribe (no banner — sidebars
host editor surfaces per tab; consumers handle lock UX inside
individual tabs via useObjectLock directly).
Tests:
- useObjectSubscription.spec.js — 4 tests covering subscribe on mount,
unsubscribe on unmount, reactive id flips, enabled gate.
- useObjectLock.spec.js — 10 tests covering REQ-CO-LOCK-002 / 003 / 004
(cache-derived state, acquire/release HTTP semantics, typed errors,
beforeunload beacon, scope-dispose release).
Full suite: 893 / 893 (one pre-existing CnMapWidget leaflet-missing
suite failure is unrelated). check:docs green.
Lock-on-edit auto-acquire (wiring acquire/release into the form
dialogs) is intentionally deferred to a follow-up cycle; the
composables are public so early adopters can wire it themselves.
* feat(CnAppRoot): app-availability guard (defaults to OpenRegister) (#191)
* feat(CnAppRoot): app-availability guard (defaults to OpenRegister)
Add a `requiresApps` prop on CnAppRoot that defaults to
`['openregister']` and renders an `<NcEmptyContent>` empty-state
when any required Nextcloud app is missing from the capabilities
payload. The guard is on by default for the entire fleet
(decidesk, pipelinq, procest, zaakafhandelapp, softwarecatalog);
apps that don't depend on OpenRegister opt out via
`:requires-apps="[]"`.
OpenSpec change: openspec/changes/cnapproot-app-availability-guard/
REQ-OR-1 defaults to ['openregister']
REQ-OR-2 capabilities-API check on mount, one round-trip
REQ-OR-3 empty-state when any required app is missing
REQ-OR-4 `#or-missing` scoped slot escape hatch
REQ-OR-5 `:requires-apps="[]"` opt-out
REQ-OR-6 loading spinner during the check
REQ-OR-7 network failure falls through to renderer
Also moves `@nextcloud/capabilities` from `dependencies` to
`peerDependencies` to match the rest of the `@nextcloud/*` family
(axios, l10n, router, vue) and ratchets the CnAppRoot JSDoc
baseline from 54% to 80% by documenting all slots.
* docs(CnAppRoot): document requiresApps prop + or-missing slot
check:docs caught that CnAppRoot.md (styleguide) and
docs/components/cn-app-root.md (docusaurus mirror) didn't list the
new `requiresApps` prop or the `or-missing` slot. Added both,
cross-linking to the architecture-level guide for the opt-out
pattern.
* ci: retrigger Code Quality after stale-partial false positive
* merge: beta into cnapproot-or-guard + fix duplicate data()
Beta added the index-sidebar hoist feature (cnIndexSidebarConfig in
CnAppRoot.data()) plus CnStatsBlockWidget + dataSource on CnChartWidget
since this branch was cut. Merging surfaced two issues:
- CnAppRoot.vue ended up with two `data()` functions; the OR-guard
one was overwriting the sidebar one. Merged them into a single
`data()` returning all four fields.
- docs/components/_generated/CnChartWidget.md needed a regen (now
carries the dataSource prop) and CnStatsBlockWidget.md is new.
905/905 tests pass.
* fix(lint): drop extra blank line left by data() merge
* fix(openspec): make all in-flight changes pass strict validation
The OpenSpec validator parses the FIRST physical line of each
requirement body as the requirement text and requires it to contain
SHALL or MUST. Many committed changes had soft-wrapped first
sentences where the normative verb landed on line 2, causing
'must contain SHALL or MUST' errors despite the body clearly being
normative. Two changes had structural issues (empty MODIFIED /
REMOVED sections, and a 'Requirements' header that should have been
'ADDED Requirements').
Fixes per change:
- add-json-manifest-renderer: replaced 'Requirements' header with
'ADDED Requirements' and removed empty MODIFIED / REMOVED
sections (replaced with a non-delta 'Notes' block).
- add-live-updates-plugin: REQ-ST-LU-002/003 first-line MUST.
- cnapproot-app-availability-guard: REQ-OR-3/6/7 first-line MUST.
- manifest-abstract-sidebar: 'All additions' first-line MUST.
- manifest-card-index-component: 2 reqs.
- manifest-chart-widget: 'Requirements' -> 'ADDED Requirements'.
- manifest-config-refs: 5 reqs.
- manifest-detail-sidebar-config: 3 reqs.
- manifest-dynamic-menu: 2 reqs (one rephrased SHOULD -> MUST NOT).
- manifest-named-view-sidebar: 2 reqs.
- manifest-resolve-sentinel: 1 req.
- manifest-settings-orchestration: 4 reqs.
- manifest-settings-rich-sections: 3 reqs.
- manifest-wiki-page-type: 1 req.
All fixes preserve original intent — they only un-wrap the first
sentence (no semantic edits) except where SHOULD had to be paired
with a separate normative clause (manifest-dynamic-menu placeholder
recommendation).
Result: openspec validate --all --strict reports 46 / 46 passing.
* feat(ai-chat-companion): floating widget + composables + NcAppSidebar
Implements the nextcloud-vue side of the ai-chat-companion 3-spec
chain (hydra ADR-034). Adds a single line auto-mount via CnAppRoot —
consuming apps get a Conduction-Cobalt hex FAB and a context-aware
chat panel for free.
NEW components (src/components/CnAiCompanion/):
- CnAiCompanion.vue — top-level mount, OR health probe, no-op fallback
- CnAiFloatingButton.vue — Conduction-Cobalt (#4376fc) hex with white
AI sparkles icon (Material Design Creation), pointy-top point-up per
brand rule. Solid colour, no gradients.
- CnAiChatPanel.vue — built on NcAppSidebar (proper NC theming + a11y
+ slide animation) with NcActionButton secondary actions for Start
new chat and History.
- CnAiMessageList.vue, CnAiInput.vue — message rendering and input.
NEW dialog (src/dialogs/, per ADR-004 file-isolation rule):
- CnAiHistoryDialog.vue — NcDialog overlay listing past conversations.
NEW composables (src/composables/):
- useAiContext() — injects the reactive cnAiContext provided by
CnAppRoot.
- useAiChatStream() — owns the SSE lifecycle via @microsoft/fetch-
event-source (handles POST bodies, abort signals, automatic
reconnect, frame parsing). Falls back to POST /api/chat/send on
404/501 with synthetic final event.
MODIFIED:
- CnAppRoot.vue — provides reactive cnAiContext; auto-mounts
<CnAiCompanion /> as the last NcContent child so the embedded
NcAppSidebar slides in from the right.
- CnIndexPage / CnDetailPage / CnDashboardPage — push pageKind,
registerSlug, schemaSlug, objectUuid into cnAiContext on created()
+ watch + beforeDestroy.
- l10n/en.json + l10n/nl.json — 13 new keys + Dutch translations.
- src/index.js + src/types/index.d.ts — barrel exports.
Demo: examples/ai-chat-companion-demo/ — standalone harness with
mocked OR backend (health, /api/chat/stream, /api/chat/conversations).
Tests: Vitest mount + behaviour tests for every new component and
composable. tests/__mocks__/fetch-event-source.js mocks the SSE
library so unit tests run without a real OR backend.
Depends on the hydra ai-chat-companion spec.
* fix(ai-companion-fab): equilateral hex + no NC button chrome
CnAiFloatingButton was inheriting Nextcloud's default <button> styles
(light blue background, 8px border-radius, 8px 12px padding), and
its square 60x60 box made the pointy-top hex polygon render with
unequal sides (taller faces vs shorter top/bottom slopes).
This patch:
- Sets the button to 52x60 (an exact √3:2 ratio) so the six vertices
of the polygon land on equal-length sides — a true equilateral
pointy-top hexagon per Conduction brand rules.
- Strips every inherited NC button style: padding, margin, border,
border-radius, background, box-shadow — !important blocks because
the same problem we hit with .cn-ai-companion: webpack bundles both
the package's CJS and ESM CSS into one consumer bundle and the
cascade is unpredictable.
- Forces the hex inner span to fully cover the button (100% x 100%)
and explicitly removes any inherited border-radius.
Visually: no more light blue rectangle around the hex; the icon sits
inside a clean, equilateral Conduction-Cobalt hex.
* fix(useAiChatStream): render assistant reply on `final`-only SSE streams
When the OpenRegister side ships the non-streaming-provider degradation
path (one `final` event with `fullText`, no preceding `token` events),
the previous finalise() pushed an empty `content: ''` message into the
state because state.currentText was never populated. The assistant
bubble rendered as empty.
Two related defensive fixes:
- In the `final` handler: if no `token` events arrived, seed
state.currentText from parsed.fullText before finalising. This matches
the hydra spec's contract clause: "Non-streaming LLPhant providers
MUST degrade gracefully: emit zero `token` events and exactly one
`final` event carrying `fullText`."
- In finalise(): use the server's parsed.messageId as the message's
Vue render-key id when present; synthesise a stable client-side id
(`client-<ts>-<rand>`) when the server sends an empty messageId.
Pairs with openregister#1466's follow-up that now propagates the
persisted assistant message's id through the `final` event.
End-to-end: real LLM round-trip through the decidesk pilot now renders
the assistant bubble with the qwen3.5 reply visible.
* fix(useAiChatStream): send `message` field name to match OR controller
OR's ChatStreamController (PR #1466) reads `$body['message']` to match
the existing /api/chat/send shape. The widget was sending `content:`,
so every stream request returned `{isError: true, error: missing_message}`
and the assistant bubble never rendered.
Send both `message` and `content` (alias) so the widget stays
compatible across the rollout window. OR's controller ignores unknown
keys.
* feat(openspec): add nextcloud-vue-in-memory-manifest change
Add OpenSpec artifacts (proposal, specs, design, tasks) for the new
in-memory call shape of useAppManifest. The composable will accept a
{ manifest, validate? } options object, skipping all backend fetching
when the manifest is provided directly. The legacy positional signature
useAppManifest(appId, bundledManifest, options?) is preserved verbatim,
so no current consumer requires changes.
The new overload unblocks OpenBuilt's virtual app builder, which today
works around the missing call shape by redirecting options.endpoint to
a per-slug stub URL (see openbuilt bootstrap-openbuilt change).
Capability: in-memory-app-manifest-loader (new)
- REQ-IMM-001: in-memory call shape returns input manifest synchronously
- REQ-IMM-002: no backend fetch in the in-memory branch
- REQ-IMM-003: optional client-side validateManifest pre-mount
- REQ-IMM-004: legacy positional signature preserved
Also switches openspec/config.yaml from schema: conduction (not shipped
with the CLI) to schem…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First fleet-wide consumer of the @conduction/nextcloud-vue v1.x manifest renderer. 18 of 20 pages migrated off
type: "custom"; 1 documented exception (LiveMeeting).Four commits
b5c88cd2src/manifest.jsonto schema v1.2.0 — index/detail/dashboard/settings/custom types. TODO markers on 18 obsolete view files.4b49bca1<CnAppRoot>inApp.vue, build router from manifest inmain.js, createcustomComponents.jsregistry. Settings page migrated totype:"settings"with rich sections (version-info+register-mappingwidgets) — was the hold-out lib gap, now solved.9494e546MeetingAgendaTab,MotionAmendmentsTab,DecisionActionItemsTab+ 6 add/remove or read-only variants).ed34703c@conduction/nextcloud-vue@1.0.0-beta.5+ webpack alias for@nextcloud/axios(exports-field workaround, transitive interop).Page mapping (final)
indexdetaildashboardsettingscustomVerification (against the published lib)
customComponents registry survivors
The single Vue file remaining in the registry beyond the relation tabs is
LiveMeetingView— realtime meeting shell, WebSocket subscriptions, frame-by-frame state. Documented exception, won't change.Sidebar tabs (all 9 implemented)
Full CRUD:
MeetingAgendaTab,AgendaMotionsTab,MotionAmendmentsTab,DecisionActionItemsTab. Add/remove relations:GovernanceBodyMembersTab,MeetingParticipantsTab,MinutesSignersTab. Read-only listings:MotionVotesTab(vote authoring is LiveMeeting-only by design),AmendmentParentMotionTab(single-ref display + click-through).Out of scope (not in this PR)
@nextcloud/axiosalias is needed today because of an upstreamexports-field shape in@nextcloud/axiosvs@nextcloud/vue's CJS bundle. Drop when those packages align.t('decidesk', ...)keys without nl translations. Tracked by TODO comments; needs a follow-up po-file pass.Related PRs
nextcloud-vue#131(lib v1.x manifest changes — merged to beta and published as1.0.0-beta.5)hydra#241(ADR-024 cross-link — merged or pending)