Release: merge beta into main - #436
Conversation
@nextcloud/password-confirmation@5 imports `spawnDialog` from @nextcloud/dialogs, which dialogs@7 no longer exports — v6 of the package drops that import. @nextcloud/l10n@2 lacks `detectLanguage`, which the Vue-3 stack calls during boot; a duplicated nested copy masked it until dedupe. Verified in a browser against an isolated Nextcloud 34: the SPA mounts and renders real UI (dashboard empty-state) with zero page errors.
vuedraggable v4 (the Vue-3 release) requires rows to come from an `#item` scoped slot; a v-for in the default slot throws "draggable element must have an item slot" at render. `item-key` replaces the manual :key binding. Migrated OrgNavigationEditor and OrgNavigationEditorRow (nested children). Found by running the e2e suite against an isolated Nextcloud 34.
…orm model API
Three silent-failure classes, none of which Vue 3 warns about:
1. `DashboardSwitcherSidebar` declared the Vue 2 component option
`model: { prop: 'isOpen', event: 'update:open' }` so the host could
write a bare `v-model`. Vue 3 removed that option outright: the bare
`v-model` in Views.vue compiled to `modelValue` / `update:modelValue`,
neither of which the sidebar declares. `isOpen` therefore stayed at
its initial `false` for the lifetime of the page — clicking the
hamburger toggled the host's state but the panel never got the
`open` class, so the dashboard switcher was unreachable and every
spec that drives it (sidebar → row cog → Add custom widget) died in
`beforeEach`. The host now binds `:is-open` and `@update:open`
explicitly; the sidebar's public contract is unchanged.
2. `.sync` was removed in Vue 3. The compiler silently drops the unknown
modifier, leaving a one-way bind, so `:show.sync` / `:value.sync` /
`:checked.sync` rendered but never wrote back.
3. `@nextcloud/vue@9` renamed the two-way prop of every form control
from `value` / `checked` to `modelValue` and the event from
`update:value` / `update:checked` to `update:modelValue`. The old
names fail silently — `:value` falls through as a plain attribute and
the old event is never emitted. 60 sites across 16 files, including
every widget config form (label, live-tile, iframe, clock, weather)
and the visibility rule editor.
Listeners are camelCase on purpose: `useModel()` matches a literal
`onUpdate:modelValue` in the vnode props, so `@update:model-value`
reads as uncontrolled and the parent never receives the update.
…e selectors
Two distinct Vue-2→3 mechanisms, plus accumulated harness rot.
1. `@input` on <NcSelect>/<NcSelectTags> is DEAD under @nextcloud/vue@9.
v9's NcSelect declares `emits: [" ", "update:modelValue"]` and emits only
`update:modelValue` — it never emits `input`. Under Vue 2 the component
emitted `input` (the v2 v-model event), so `@input="handler"` was the
documented way to observe a selection.
Worse than merely dead: because `input` is NOT in the component's `emits`
list, Vue 3 treats `@input` as a *native* listener and falls it through to
the component's root element. NcSelect's root wraps the `<input
class="vs__search">` search box, whose native `input` events bubble — so
the handler fired on every KEYSTROKE, with a DOM Event as its payload
instead of the selected option.
That is exactly how the dashboard-sharing sharee picker produced a share
row with a blank name: `onShareeSelected` ran on a keystroke and got an
Event, so `displayName`/`shareWith` were undefined.
An earlier pass had renamed the PROP half of this API (`:value` →
`:model-value`) but left the EVENT half as `@input` — ClockWidgetForm even
carries a comment describing the v9 rename above three still-broken
handlers. This commit finishes that migration: 15 listeners across 9
components now use `@update:modelValue` (camelCase — the kebab form is a
known silent-failure trap in this codebase, and camelCase is already the
established convention here at 49 existing usages).
Left alone deliberately: `@input` on <CnIconBrowser> and on plain native
<input> elements. CnIconBrowser really does declare the Vue-2 `value`/
`input` pair, so those bindings are correct.
2. Admin "Group dashboards" tab rendered its empty state on a 200 response.
`GET /api/admin/groups` answers with the documented envelope
`{active, inactive, allKnown}`, but the store did
`res.data?.data ?? res.data ?? []` — the envelope OBJECT fell through to
the bare `res.data` branch, `groups.filter(...)` threw "filter is not a
function", and the catch swallowed it into a "Failed to load groups" toast.
Read `allKnown` (the field the backend documents as "the full list for the
UI to render") and guard with Array.isArray.
3. e2e harness rot (pre-existing, unrelated to Vue 3 — these specs could not
have passed on the Vue-2 base branch either, because the selectors they
assert on exist in NEITHER branch's source):
- mydash→launchpad rename leftovers: `/apps/mydash`, `.mydash-sidebar-
toggle`, `.mydash-edit-mode`, `.mydash-container`.
- widgets that moved into nc-vue and gained the `cn-` prefix:
`.label-widget`→`.cn-label-widget`, `.text-display-widget`→
`.cn-text-widget`, `.image-widget`→`.cn-image-widget`,
`.nc-widget-grid-picker__*`→`.cn-nc-widget-grid-picker__*`,
`.launchpad-widget__content`→`.cn-widget-wrapper__content`.
- `.launchpad-grid` never existed (the container is `.launchpad-container`,
the GridStack root is `.grid-stack`).
- nc-widget option label is "Nextcloud widget", not "Nextcloud Widget".
- the image form moved to nc-vue's CnImageWidgetForm, which dropped the
URL/Upload radio pair for a single file <label> and DEFERS upload to
commit; the specs still drove the old radio flow.
- resource-serve.spec.ts read only NEXTCLOUD_URL, so it silently targeted
the default :8080 instance instead of NC_BASE_URL.
- a local openSidebar helper used `.launchpad-sidebar-toggle,
.launchpad-floating-controls button` with `.first()`; the dashboard-cog
precedes the toggle in DOM order, so it opened the wrong popover.
One selector change IS Vue-3-caused: v9's NcTextField forwards fallthrough
attributes onto the inner <input> rather than the wrapper <div> the v8
build used, so `[data-test=…] input` matches nothing — `data-test` IS the
input now.
…rd-widget The nc-widget proxy renderer moved into nc-vue as CnNcWidgetWidget and its classes gained the shared `cn-` prefix. The spec still targeted the old launchpad-local `.nc-dashboard-widget*` names, which exist in neither branch's source, so all three REQ-WDG-019/021 tests could never find the cell. The renderer also has no dedicated `__empty` element — loading, no-widget and empty-list all render through `__state` — so the REQ-WDG-021 assertion now matches `__state` filtered on the translated 'No items available' string, keeping the scenario pinned to the empty-list case specifically rather than any state placeholder. All three pass live.
…e3.1 Drops the USE_LOCAL_LIB dependency on a library source checkout. 2.0.7 could not be used: the barrel re-exported each component's default straight from its .vue2.js script module, so webpack redirected past the wrapper that runs `script.render = render`, dropped the barrel, and app-root-level components mounted with no render function — a blank page with zero console errors. 2.1.0-vue3.1 anchors that export module-locally so the wiring is reached by construction. Verified by building against the PUBLISHED tarball with the local-source symlink moved aside, deploying, and loading the app authenticated.
Vue 2 required the key on each child of a `<template v-for>`; Vue 3 keys the whole fragment, so a key on a child is IGNORED and the list renders unkeyed — losing the identity Vue needs to patch rows correctly on reorder. CalendarWidget's agenda groups and FilesWidget's path breadcrumbs both used the Vue-2 form.
78 of 570 unit tests failed for six distinct Vue-2 -> Vue-3 mechanisms,
none of which surface as a compile error:
* `render(h)` — Vue 3 no longer passes `h` to the render function; it is
imported from `vue`, and the vnode data object is flat rather than
Vue 2's nested `{ attrs: {…} }`. Fixed in the shared
`@conduction/nextcloud-vue` stub and the DashboardConfigModal spec.
* `Vue.use(PiniaVuePlugin)` — there is no global `Vue` to install onto,
and a Pinia instance IS an app plugin under Vue 3. The v1 top-level
`pinia` mount option is now `global.plugins: [pinia]`; handled in the
existing vueTestUtilsCompat adapter alongside the other hoisted keys.
* Listener fallthrough — Vue 3 merges listeners into `$attrs` as `onClick`
and auto-applies them to a stub's root element. A stub that also does
`@click="$emit('click')"` therefore fires the parent handler TWICE.
The wizard advanced two steps per click and the shell hamburger toggled
itself shut. Declaring `emits: ['click']` removes `onClick` from
`$attrs`, restoring a single path. Applied to every affected stub.
* `v-model` contract — the prop is `modelValue` and the event
`update:modelValue` (was `value`/`input`, and `checked`/`update:checked`
for NcCheckboxRadioSwitch). Stubs on the old contract never wrote back,
so forms silently stayed empty.
* VTU v2 API — `wrapper.destroy()` is `unmount()`, `findAll()` returns a
plain array rather than a WrapperArray with `.wrappers`, and v1's
`scopedSlots` is now `slots` (silently ignored, so every slot in
BeheerTabs rendered empty).
* `reactive()` identity — `state.selectedWidget` hands back a Proxy, so a
bare `toBe` compared proxy to raw. `toRaw` keeps it an identity check.
Also raises `hookTimeout` to match `testTimeout`: several admin specs
resolve their component with `await import()` inside `beforeEach`, and the
first call pays Vite's full SFC transform cost, overrunning the 10s
default with "Hook timed out" and no assertion failure.
`@nextcloud/eslint-config@8` resolves eslint-plugin-vue's **Vue 2** preset
(visible via `eslint --print-config`: `vue/no-reserved-props` arrives as
`{ vueVersion: 2 }`). Four of the 17 errors were that preset forbidding
syntax Vue 3 *requires*:
- `vue/no-v-for-template-key` — Vue 2 banned a key on `<template v-for>`;
Vue 3 keys the fragment there. Replaced with the Vue-3 counterpart
`vue/no-v-for-template-key-on-child`.
- `vue/no-v-model-argument` — `v-model:arg` is Vue 3's replacement for
Vue 2's `.sync`.
`no-multiple-template-root` and `valid-v-bind-sync` are off for the same
reason. Switching wholesale to `@nextcloud/eslint-config/vue3` is not an
option here: it references `@typescript-eslint/*` rules whose plugin this
project does not register, and ESLint crashes on load.
The `vue/no-deprecated-*` family is enabled as errors, because these are
Vue-2 idioms the Vue 3 compiler ignores rather than rejects — the failure
mode is a dead listener at runtime. It immediately found one:
`DashboardRowActions` bound `@click.native.stop`, and `.native` no longer
exists in Vue 3. The component's own docblock already described the
binding as `@click.stop`, so the modifier was stale as well as inert.
`@spec openspec/...` is registered via `definedTags`. It is this repo's
ADR-003 / ADR-020 traceability tag with its own enforcing gate
(`composer lint:spec-annotations`), so the linter needed to learn the tag
rather than the convention being bent — that alone was 746 of the 1242
warnings.
Remaining errors were genuine style violations in three files, autofixed.
…ng focus
`WidgetMovePanel.vue` — the WCAG 2.1 SC 2.1.1 keyboard equivalent of
GridStack's pointer-only drag — was fully implemented, unit-tested, and
never mounted. Its only references in `src/` were its own definition and a
doc comment in `WidgetContextMenu.vue` describing wiring that was never
done: passing tests against a component the app never renders.
The pieces that existed: `WidgetContextMenu` already declared and emitted
`move`, and `nudgePlacement()` in `useGridManager.js` already implemented
the geometry as a pure function returning the clamped rect plus push-down
side effects. Only the seam between them was missing:
- `useGridManager` gains an `onMove` option and `triggerMove()`,
mirroring the existing edit/remove/visibility-rules triggers.
- `Views.vue` binds `@move`, mounts `<WidgetMovePanel>`, and persists a
confirmed rect through `updatePlacements()` — folding the moved
placement and any pushed placements into ONE call so the layout is
written atomically on the same debounced path drag already uses.
Separately, `placementItemKey()` interpolated `updatedAt` and a JSON dump
of `styleConfig` into the per-item render key. Every persist therefore
changed the key, Vue tore down and recreated the grid item's DOM node, and
whatever was focused inside it lost focus. That defeats keyboard
repositioning even once wired: after a single arrow-key move focus fell
back to the document and a second move needed a full re-navigation of the
grid. The key is now the placement id alone — a content change re-renders
through prop updates without needing a new key.
Two distinct causes behind the jsdoc warnings.
The larger one was structural: ~60 methods carried a COMPLETE docblock
immediately followed by a second, `@spec`-only block. Only the block
adjacent to the function is read, so the real `@param`/`@return` docs
were invisible to both the linter and any reader's tooling. Merging the
`@spec` tag into the block above it recovers the existing documentation
rather than rewriting it.
The rest were genuinely undocumented parameters, concentrated in the HTTP
wrapper layer (`services/api.js`) and the admin group-priority editor.
Written out properly — no `@param {*}` filler.
Completes eslint to 0 errors / 0 warnings across `src/`.
The last three warnings were `vue/no-v-html`. Both sites are legitimate and
stay, with a scoped disable and the reason recorded:
- DashboardFooter renders admin-authored HTML that only reaches the
client through `FooterService::sanitiseHtml()`.
- NewsWidget renders third-party RSS, sanitised server-side by
`NewsWidgetService::sanitiseSummaryHtml()` and again through DOMPurify
in `formattedSummary()` after truncation.
A `disable`/`enable` pair is used rather than `disable-next-line` because
the rule reports on the `v-html` attribute's own line, which is several
lines below the element's opening tag in both files.
`npm run lint` is a four-step chain, and its third step was failing on a
pre-existing REQ-INIT-003 violation in `src/public.js`. That call was worse
than a style breach — it was dead. It read a `public-share-token`
initial-state key that no PHP ever provides (`PageController::publicShare`
renders the template with no `provideInitialState` call), so it always
returned its fallback and the URL path parse below it did the real work,
exactly as `templates/public.php` documents. The comment above it asserted
the opposite. Removed the dead read and documented the actual source; the
page is anonymous, so adding the key to the initial-state contract would
have meant a server change with no consumer.
NewsWidget renders feed summaries through `v-html`, so how they are shortened is a security property rather than a formatting detail — and the file had no unit spec at all. Covers what the previous raw-offset slice got wrong: the budget counts visible characters rather than markup bytes, a cut that lands inside a tag or attribute can no longer emit a truncated anchor, and the `rel="noopener noreferrer"` REQ-NEWS-005 forces survives truncation. Also pins the client-side re-sanitise, so script/img/onerror payloads are stripped even if the server-side pass were bypassed.
Carries the CnDashboardGrid keyboard a11y this app's WCAG 2.1 SC 2.1.1 e2e coverage depends on, plus three crash fixes — notably CnLockedBanner, whose `message` prop used a `default()` factory reading `this.lockedBy`. Vue 3 invokes default factories with no `this`, so it threw on mount and white-screened any page rendered while another session held a lock. Pinned explicitly rather than via `npm install`: the lockfile had vue3.2 resolved, and the caret range does not force a prerelease bump on its own.
The stub's docblock explained why it exists — the published CJS bundle `require()`s `.vue` files Vite cannot transform — but not what accepting it gives up. The alias in vitest.config.js redirects EVERY import of `@conduction/nextcloud-vue` to the stub, so not one of this suite's 624 unit tests runs against the real library. A green unit run is evidence that the stub works, not that the library does; the suite structurally cannot catch a breaking prop change, a renamed event, or a component that renders nothing. The last case is not hypothetical. Earlier in this migration the library shipped a dist whose render wiring had been tree-shaken away and every `Cn` component rendered as a silent comment node — blank app, zero console errors — while the unit suite stayed fully green, because these stubs render fine. Only the browser caught it. Recorded at both places a reader lands: the stub docblock and the alias. Library integration is the Playwright suite's job, and nothing else's.
…k fixture
- src/composables/useGridManager.js: DEFAULT_MENU_HEIGHT/WIDTH were sized
for the original 3-button context menu (132px/150px). The popover has
since grown to 5 items (Edit, Move, Visibility rules…, Remove, Cancel)
and measures ~232.5x156.5px in practice, so the viewport-edge clamp
under-estimated its footprint and let the real popover render past the
clamped edge (or push ctx-remove out of the fixed-position viewport
entirely). Bumped both constants above the measured size with margin.
- tools/deploy-to-launchpad.sh: two pre-existing bugs hit while
redeploying to verify the above.
1. The JS-bundle rename loop computes `new` from `f` via a sed that is
now a no-op (source/target app ids are both "launchpad"), so
`sed ... "$f" > "$new"` opened-and-truncated its own input before
reading it, and the trailing `mv "$f.LICENSE.txt" "$new.LICENSE.txt"`
failed outright ("are the same file"). Now stages sed's output in a
temp file and only renames the LICENSE sidecar when the path
actually differs.
2. `custom_apps/launchpad` is itself a bind mount on this dev box;
`rm -rf "$DEST"` deletes every file underneath but then fails on the
mount point itself ("Device or resource busy") — AFTER already
wiping the directory's contents, leaving the shared dev instance's
launchpad app empty until the next successful deploy. Now clears
$DEST's contents (`find "$DEST" -mindepth 1 -delete`) instead of the
directory itself.
- tests/e2e/fixtures/acknowledgements.ts (new) +
tests/e2e/dashboard-acknowledgements.spec.ts: REQ-ACK-001 ships no
admin-facing form UI for declaring an acknowledgement requirement in
this pass (config path is @e2e excluded) — nothing in the app seeds a
compulsory widget with an outstanding acknowledgement for the e2e specs
to observe. Added ensureOutstandingAcknowledgement(), which seeds one
through the real, authorised PUT /api/widgets/{id} contract (same
pattern as fixtures/role-feature-permissions.ts) and wired it into a
beforeAll so REQ-ACK-002 (forced-delivery gate) and REQ-ACK-004
(read-receipt report) have real state to exercise. Idempotent: reuses
the marked placement and bumps acknowledgementContentVersion each run
so a prior run's receipt never leaves it satisfied.
- package.json / package-lock.json: consume
@conduction/nextcloud-vue@2.1.0-vue3.4 (already in progress before this
session).
…l safety
- src/main.js: nc-vue's CnNcWidgetWidget self-registers `nc-widget` with
`form: null` ("CnNcWidgetWidgetForm is not yet present in this tree" —
stale; CnNcDashboardWidgetForm + CnNcWidgetGridPicker DO exist in this
nc-vue version, just never wired to the registration). listWidgetTypes()
filters null-form entries, so Add Widget's type picker silently never
offered "Nextcloud widget". Complete the registration from launchpad
using nc-vue's own public last-registration-wins registry API — the
same pattern nc-vue itself uses to re-register table/object-list/map.
- src/views/Views.vue: CnNcDashboardWidgetForm injects a `widgets` catalog
for its grid picker; nothing provided it, so it always saw the inject
default ([]) and rendered empty regardless of the form fix above. The
widget store already fetches this exact Nextcloud-native dashboard-widget
list (loadAvailableWidgets — its own comment notes it feeds
CnNcWidgetWidget's runtime renderer). provide('widgets', ...) it down
from Views.vue's setup() so the picker has real data.
- src/styles/workspace.css: CnLabelWidget renders a flex container with a
<span> text child; flex items default to min-width:auto, which floors
the span's size at its longest unbreakable word regardless of the
component's own overflow-wrap:break-word, so a very long single word
overflows the widget cell. Patched with an additive (non-conflicting)
min-width:0 rule, the same flex-child pattern already used for
.launchpad-workspace above it in this file.
- tests/e2e/widget-context-menu.spec.ts: right-edge and bottom-edge popover
tests picked ".grid-stack-item.first()" and clicked via raw
page.mouse.click(x, y) at its boundingBox() coordinates. GridStack
positions items via gs-x/gs-y, not DOM order, so on this long-lived,
heavily-populated shared dashboard the "first" item can sit well below
the fold; toBeVisible() doesn't catch that (it's a CSS-visibility check,
not a viewport-scroll check), so the click silently landed off-screen
and the popover never opened. scrollIntoViewIfNeeded() before reading
the box fixes it without touching any assertion.
tools/deploy-to-launchpad.sh resolved its target container with `docker ps -qf name=nextcloud` — a SUBSTRING match. On a box that also runs a shared `nextcloud` dev container (used by other apps/worktrees), that substring silently matched the WRONG container: one that bind-mounts `custom_apps/launchpad` straight onto a real host checkout. Every "successful" deploy this session wrote through that mount onto disk instead of reaching `lp-vue3-e2e` (the container the e2e suite actually points at via NC_BASE_URL=http://localhost:8098) at all — the suite kept testing a stale bundle while every app-source fix silently landed on a real checkout instead of a disposable instance. Fixes: - Resolve the target via `LAUNCHPAD_DEPLOY_CONTAINER` (default `lp-vue3-e2e`), verified by exact name/id via `docker inspect`, never a substring filter. - Refuse to deploy — loudly, before touching anything — if the resolved container's app directory (or any ancestor of it) is a BIND mount. A bind mount means the container is wired onto a real host path; a volume or a plain in-container directory is fine. This is the general guard: even with the container-name bug fixed, nothing should silently write through to someone's checkout again. Verified both directions against the live containers on this box: LAUNCHPAD_DEPLOY_CONTAINER=nextcloud is now refused (bind mount detected at custom_apps/launchpad), and the lp-vue3-e2e default proceeds (volume only, no bind mount in its ancestry).
Picks up nc-vue PR #558 (fix(CnIndexPage): scroll the table, not the page column, in table view), merged into feat/vue-3 this morning. Folded into the same re-baseline cycle as the deploy-target fix so there is one clean measurement against the library version the app will actually ship with, rather than two rounds.
Picks up the CnWalkthrough persistence fix: completionConfigKey was schema-declared, documented, and spec-mandated, but had neither a read nor a write path wired up (grep only hit the schema file) — so dismissal was localStorage-only and the tour reopened in every fresh browser context. Full e2e re-run at this pin: 73 passed / 1 failed / 4 skipped.
…vue3.7 - tests/e2e/widget-context-menu.spec.ts: the right/bottom-edge popover tests read boundingBox() immediately after page.setViewportSize(), but that resize triggers a GridStack column reflow and every .grid-stack-item animates to its new position over GridStack's own 300ms CSS transition (.grid-stack-animate, gridstack.css). A box read mid-transition describes an in-flight frame, not the rest position — harmless for a click well inside a widget, but these tests deliberately click within 20-30px of the edge, so the animation delta was enough to land the click in the grid gutter and never open the popover. Measured 2-in-3 failure rate before; added waitForStableBox() (polls until two consecutive reads match, no fixed sleep) and verified 3/3 clean runs after. - tests/e2e/fixtures/secondary-user.ts (new): throwaway-user provisioning (OCS API), known-password reset for a pre-seeded account, and a loginAs() that authenticates a SECOND, genuinely separate session. storageState: undefined is load-bearing: browser.newContext() otherwise inherits playwright.config's top-level use.storageState (the shared admin session), so every login attempt was silently already authenticated as admin and never hit the login form at all. - tests/e2e/runtime-shell-canEdit.spec.ts: enabled the two empty-state scenarios (provision a real zero-dashboard account, toggle allowUserDash, assert). Also fixed setAllowUserDashboards: it was issuing its PUT through the cookie-authenticated built-in `request` fixture, which Nextcloud's CSRF check rejects outright on a state-changing route (measured: 412) — the failure was swallowed by a console.warn instead of failing the test, so the "false" scenario ran against whatever the flag happened to still be. Rewritten to use its own Basic-Auth + OCS-APIRequest admin context (matching allow-personal-dashboards-flag.spec.ts) and to throw on a non-OK response instead of warning. - tests/e2e/active-dashboard-resolution.spec.ts: enabled the fresh-user empty-state scenario using the same throwaway-user fixture. - tests/e2e/dashboard-sharing.spec.ts: investigated and restored the recipient-visibility skip with an accurate reason (two independently confirmed causes, neither a stale selector): DashboardResolver (lib/Service/DashboardResolver.php) only resolves a user's active dashboard from owned/group/template rows and never considers dashboards merely shared to them, so a recipient with no dashboard of their own lands on the empty state regardless of what's shared; and the ADR-023 action-authorization matrix defaults every action to admin-only, with nothing on this instance ever broadening it — confirmed live via OCSForbiddenException on the account's first non-admin AJAX call. Both are real product/environment considerations for a follow-up change, not something one spec's fixture should paper over. - package.json / package-lock.json: consume @conduction/nextcloud-vue@2.1.0-vue3.7 (nested-modal stacking fix). Checked launchpad's modal usage — every NcModal/NcDialog/Cn*Modal is a sibling in Views.vue's template, none nested inside another, so this is expected to be a no-op here; full e2e run confirms no regression.
feat(vue3): migrate Launchpad to Vue 3 and @conduction/nextcloud-vue 2.1.0-vue3.7
…admin action baseline
Defect 1 — a share was inert. DashboardMapper::findVisibleToUser() unions
exactly three buckets (owned rows, group_shared rows in the user's groups,
the 'default' sentinel); share rows were in none of them. A recipient with
no dashboard of their own landed on the empty state and the shared
dashboard was unreachable everywhere: not in the switcher, never a
resolution candidate, not fetchable by id.
DashboardResolver now owns the share lookup (findSharedLevels,
findSharedDashboards, tryGetSharedDashboard) and DashboardService folds it
into getVisibleToUser(), resolveActiveDashboard() (new last-resort step 6b)
and getEffectiveDashboard() (before the writing template step). Precedence:
a share is the LAST candidate, so it can never displace anything the user
owns, reaches through a group, or explicitly selected. Shared results carry
the SHARE's permission level, not the owning row's.
Defect 2 — the app was unusable by non-admins on a fresh install. The
ADR-023 seed mapped every declared action to ["admin"], so a non-admin hit
'Action dashboard.list requires admin rights' on the first AJAX call —
including the "Create your first dashboard" CTA the empty state shows to
exactly those users. Added a GROUP_ALL_USERS ('@ALL') sentinel, granted it
to the ordinary end-user surface in the seed, kept every administrative
action admin-only, and added a version-gated ApplyActionBaseline repair
step so already-installed instances get the baseline too without
overwriting admin customisation.
Tests: 28 new PHPUnit tests, all verified to fail against the unfixed code
(Defect 2 fails behaviourally with the real OCSForbiddenException).
…nd the @ALL matrix column Both suites verified to FAIL against origin/development's components: - DashboardSwitcherSidebar: 4 of 5 new tests fail (shared rows leak into the primary-group section, no data-section="shared", switch emits no shared discriminator, section order is [group, default, user]). - ActionAuthMatrix: 2 of 6 fail (displayGroups is ['admin','editors'] — the @ALL column the shipped baseline needs is never rendered, so an admin can neither see nor revoke the non-admin grant).
…ipient e2e The server-side fix alone was not enough end-to-end. Every source-aware getter in the dashboard store filters by an EXPLICIT source value, and Views.vue's sidebarGroupDashboards only concatenated the group and default buckets — so 'shared' rows survived the server-rendered initial state but vanished the moment the store refreshed from /api/dashboards/visible. Added a sharedWithMeDashboards getter, wired it into the sidebar input and into activeDashboardSource so the row cog gates owner-only entries correctly. e2e: un-skipped 'recipient sees the shared dashboard in their switcher' (both stated blockers are now fixed) and added two ADR-023 scenarios in runtime-shell-canEdit.spec.ts — an ordinary user may call the end-user API surface but not instance analytics, and the empty-state Create CTA the app itself offers actually returns 200 and persists. Store spec verified failing against origin/development's store (rc=1); 30/30 pass with the getter.
…ally runs A repair step only executes when NC sees a version increase. The e2e instance was confirmed live to still hold the all-admin matrix with `actions_baseline_version` unset while installed_version already equalled info.xml — so without this bump `occ upgrade` is a no-op, the baseline never reaches an existing install, and the fix would have looked shipped while changing nothing.
toHaveCount(0) on .workspace-shell__empty passes trivially on a blank page, before Vue mounts. Wait for the app to render EITHER the shell or the empty state first, then assert it is not the empty state.
e2e caught what every unit test here missed. DashboardFactory sets
STATUS_DRAFT on every dashboard it creates (all 15 rows on the e2e
instance are drafts), and filterByPublicationState() hides drafts from
non-owners — so the previous commit appended the share and then filtered
it straight back out. Live: GET /api/dashboards/visible returned
{items:[]} for the recipient while GET /api/dashboard returned the
dashboard, because tryGetSharedDashboard() never went through the filter.
Two paths, opposite answers, and the switcher showed nothing.
Shares are now appended AFTER the publication filter and are exempt from
the draft/scheduled hide: a share row is the owner's explicit, named
grant to that specific user — the same class of entitlement that already
exempts the owner and admins, and what PermissionService::resolveAccessLevel()
already honours. A user WITHOUT a share still cannot see a draft
(testUnsharedDraftStaysHiddenFromNonOwners). Due scheduled rows still
materialise in memory via the extracted materialiseDueSchedule().
Root cause of the miss: the unit fixtures called setPublicationStatus(
STATUS_PUBLISHED) explicitly, so they were unrepresentative of what the
factory produces. The new test builds through DashboardFactory and
asserts the factory default, so a change there fails a unit test rather
than a browser.
Also fixes a made-up URL in the new ADR-023 e2e test: it hit
/api/analytics/instance-summary (404) instead of the real
/api/admin/analytics/summary — a 404 proves nothing about authorization.
…est non-admins
There is NO authorization hole. The reported 200 from the admin-only
instance-analytics endpoint was my own test authenticating as admin.
pwRequest.newContext() inherits playwright.config's top-level
use.storageState — the shared ADMIN session cookie from global-setup — so
Nextcloud authenticated the request by cookie and never looked at
httpCredentials at all. This is the exact trap loginAs() already documents
for browser.newContext() in this same file. Measured three ways against
the deployed code: a freshly provisioned zero-group user gets 403 on
/api/admin/analytics/{summary,dashboards/top,export}; unauthenticated gets
401; only an admin gets 200.
Consequences and fixes:
- storageState: undefined, so the context is genuinely the throwaway user.
- httpCredentials send:'always' — NC replies with a bare 401 carrying no
WWW-Authenticate header, so Playwright's default challenge-response mode
has nothing to respond to and would never send the credentials.
- the end-user-surface assertions were not.toBe(403), which also passes on
401 and 404; they now assert toBe(200) so 'never authenticated' and
'wrong URL' can no longer read as success. Both false greens really
happened while writing this spec.
- the Create CTA expects 200 or 201; 201 Created is correct for that POST.
401/403/404/5xx still fail.
loginAs(): click() also waits for the navigation it schedules, bounded by
actionTimeout (10s), NOT navigationTimeout (60s). The NC login POST +
redirect + hydration exceeds 10s on a loaded box, so the click failed while
the login had SUCCEEDED — the saved snapshot at failure shows a fully
authenticated page (#app-dashboard, Applications nav, Settings menu), not
the login form. Handed the wait to the explicit waitForSelector('#header')
that already followed it with its own 45s budget. Not a timeout increase:
if the login really fails, #header never appears and it still fails.
…bac-baseline fix(sharing,rbac): make a shared dashboard actually reachable, and a fresh install usable by non-admins
Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Bumps [stylelint](https://github.com/stylelint/stylelint) from 15.11.0 to 17.14.1. - [Release notes](https://github.com/stylelint/stylelint/releases) - [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md) - [Commits](stylelint/stylelint@15.11.0...17.14.1) --- updated-dependencies: - dependency-name: stylelint dependency-version: 17.14.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@vue/test-utils](https://github.com/vuejs/test-utils) from 2.4.11 to 2.5.0. - [Release notes](https://github.com/vuejs/test-utils/releases) - [Commits](vuejs/test-utils@v2.4.11...v2.5.0) --- updated-dependencies: - dependency-name: "@vue/test-utils" dependency-version: 2.5.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.13 to 3.4.14. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](cure53/DOMPurify@3.4.13...3.4.14) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.14 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 3.2.7 to 4.1.11. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.11/packages/coverage-v8) --- updated-dependencies: - dependency-name: "@vitest/coverage-v8" dependency-version: 4.1.11 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
) Bumps [@cyclonedx/cyclonedx-npm](https://github.com/CycloneDX/cyclonedx-node-npm) from 5.0.0 to 6.0.1. - [Release notes](https://github.com/CycloneDX/cyclonedx-node-npm/releases) - [Changelog](https://github.com/CycloneDX/cyclonedx-node-npm/blob/main/HISTORY.md) - [Commits](CycloneDX/cyclonedx-node-npm@v5.0.0...v6.0.1) --- updated-dependencies: - dependency-name: "@cyclonedx/cyclonedx-npm" dependency-version: 6.0.1 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [vue](https://github.com/vuejs/core) from 3.5.41 to 3.5.42. - [Release notes](https://github.com/vuejs/core/releases) - [Changelog](https://github.com/vuejs/core/blob/main/CHANGELOG.md) - [Commits](vuejs/core@v3.5.41...v3.5.42) --- updated-dependencies: - dependency-name: vue dependency-version: 3.5.42 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [phpmetrics/phpmetrics](https://github.com/phpmetrics/PhpMetrics) from 2.9.1 to 2.11.0. - [Release notes](https://github.com/phpmetrics/PhpMetrics/releases) - [Changelog](https://github.com/phpmetrics/PhpMetrics/blob/master/CHANGELOG.md) - [Commits](phpmetrics/PhpMetrics@v2.9.1...v2.11.0) --- updated-dependencies: - dependency-name: phpmetrics/phpmetrics dependency-version: 2.11.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…408) Bumps [phpcsstandards/phpcsextra](https://github.com/PHPCSStandards/PHPCSExtra) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/PHPCSStandards/PHPCSExtra/releases) - [Changelog](https://github.com/PHPCSStandards/PHPCSExtra/blob/develop/CHANGELOG.md) - [Commits](PHPCSStandards/PHPCSExtra@1.5.0...1.5.1) --- updated-dependencies: - dependency-name: phpcsstandards/phpcsextra dependency-version: 1.5.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…407) Bumps [squizlabs/php_codesniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) from 3.13.6 to 4.0.4. - [Release notes](https://github.com/PHPCSStandards/PHP_CodeSniffer/releases) - [Changelog](https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/4.x/CHANGELOG-3.x.md) - [Commits](PHPCSStandards/PHP_CodeSniffer@3.13.6...4.0.4) --- updated-dependencies: - dependency-name: squizlabs/php_codesniffer dependency-version: 4.0.4 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [twig/twig](https://github.com/twigphp/Twig) from 3.27.0 to 3.28.0. - [Release notes](https://github.com/twigphp/Twig/releases) - [Changelog](https://github.com/twigphp/Twig/blob/3.x/CHANGELOG) - [Commits](twigphp/Twig@v3.27.0...v3.28.0) --- updated-dependencies: - dependency-name: twig/twig dependency-version: 3.28.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.8 to 2.2.9. - [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits) --- updated-dependencies: - dependency-name: phpstan/phpstan dependency-version: 2.2.9 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [nextcloud/ocp](https://github.com/nextcloud-deps/ocp) from 34.0.2 to 34.0.3. - [Commits](nextcloud-deps/ocp@v34.0.2...v34.0.3) --- updated-dependencies: - dependency-name: nextcloud/ocp dependency-version: 34.0.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…260830084126 chore(sync): carry beta back into development
beta held 19 commit(s) development did not. Merged with -s ours: development's tree is kept BYTE FOR BYTE and only the ancestry is recorded. That is the payload -- without it the merge base never moves and the next development -> beta promotion conflicts on the version file exactly as before. 13 of 19 promotion PRs were CONFLICTING for this reason. Nothing is silently imported. What beta holds and development does not, and which this deliberately does NOT bring over: lib/Settings/launchpad_register.json Those are dead Forgejo/Codeberg CI (removed from development on 2026-08-24/25 by 'chore(ci): remove dead Forgejo/Codeberg CI configuration'), generated Docusaurus build output, and community-health files that never existed on development. Each can be added deliberately if wanted; resurrecting them as a side effect of a sync is how a merge silently undoes a decision.
…0841 chore(sync): record beta's ancestry on development
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Two dependabot bumps landed on development on 2026-08-30 without CI, and each leaves the tree unresolvable: - @vitest/coverage-v8 went to 4.1.11 while vitest and @vitest/ui stayed on ^3.2.7, so coverage-v8 peers vitest 4.1.11 against @vitest/ui's vitest 3.2.7 - stylelint went to 17.14.1, which @nextcloud/stylelint-config 2.4.0 cannot peer: it wants ^15.6.0 Both are reverted to the version the rest of their own ecosystem is on, rather than bumping the ecosystem, because a vitest 3 to 4 move is a separate change that deserves its own testing. Verified with npm ci --dry-run from the committed lock: exit 0. Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
…426) * fix(deps): hoist @vue/server-renderer so the unit suite can run Every one of the 63 frontend test files failed, with no tests executed at all: Error: Cannot find package '@vue/server-renderer' imported from node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js The package was installed, but NESTED at node_modules/vue/node_modules/@vue/server-renderer rather than hoisted. @vue/test-utils declares it as a PEER dependency ('3.x'), and a peer is resolved upward from the importing package's own directory -- so @vue/test-utils looked for node_modules/@vue/server-renderer, which did not exist. Nothing was missing from the lockfile; it was in the wrong place. Declaring it directly pins it at the top level, which is where a peer has to be. Version tracks vue itself (^3.5.42). Verified locally: reproduced the failure first (1 file, 'no tests'), then after the change the full suite runs -- 63 files passed, 682 tests passed, 0 failed. * fix(stylelint): extend the config the app actually declares stylelint exited 78 -- a CONFIGURATION failure, not a lint finding: Could not find "stylelint-config-recommended". stylelint.config.js extended 'stylelint-config-recommended-vue', which was never declared in package.json. It was present only transitively, and the config it in turn extends, stylelint-config-recommended, was not installed at all. launchpad already declares @nextcloud/stylelint-config ^2.4.0, and that is what openregister, opencatalogi, dossiq and shillinq all extend. This points the config at the package the app declares and the fleet uses, rather than adding two more dependencies to prop up an outlier. That made stylelint RUN, which surfaced 44 real violations the configuration error had been hiding. 42 were auto-fixable (rule-empty-line-before, plus a few over-indented selector continuation lines) and were fixed with --fix; the diff outside css/ is whitespace only. The last two were a genuine CSS bug in css/header-override.css: background-color: #ffffff !important; background-image: none !important; background: #ffffff !important; <- discards both of the above The shorthand alone already sets the colour and resets background-image to none, so keeping only it preserves the computed result EXACTLY. Keeping the longhands instead would not have, because the shorthand also resets the other background sub-properties. Verified: stylelint now exits 0. * fix(stylelint): let Prettier own whitespace, and stop the fixer loop The previous commit made stylelint run, and it and Prettier then disagreed about the same six lines. Prettier indents a wrapped selector list; stylelint's `indentation` rule demanded 0 tabs there. Running either fixer broke the other check: npm run stylelint:fix -> Frontend Check (format) fails npm run format:fix -> Vue Quality (stylelint) fails Both `indentation` and `string-quotes` are DEPRECATED in stylelint 15 -- it prints a deprecation warning for each on every run -- precisely because formatters do this better. Turning them off resolves the conflict in favour of the tool that owns formatting and leaves stylelint judging what only it can judge: CSS semantics. The three files are re-formatted to Prettier's shape. Verified: stylelint exit 0 AND prettier exit 0 together, with the two deprecation warnings gone. --------- Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Release: merge development into beta
beta is the release candidate, 418 commits ahead of main. Every conflict resolved to beta's side, including its refactors: where beta had removed a file the removal stands rather than resurrecting a stale copy from main. Conflicts: 4 (0 took beta's content, 4 removed per beta's refactor).
launchpad is the ONLY one of the 21 fleet apps still shipping .github/workflows/branch-policy.yml. Every other app dropped it in favour of the shared ConductionNL/.github branch-protection.yml, which launchpad also already calls. The two disagree, and the stale local copy is the stricter one: it admits only 'beta' and 'hotfix/*' into main, while the shared gate also admits 'release/v*' version bumps, 'dependabot/*' and, relevant here, any 'release/*' branch. So this release pull request is refused by the local relic and accepted by the fleet standard. Two gates under one name is worse than either alone: the check that fails is named 'Branch Policy Check' while the fleet's is 'branch-protection / check-branch', so a refusal reads as the fleet gate rejecting a promotion the fleet gate actually allows. Verified with a per-repository existence check that launchpad is the sole carrier -- an earlier sweep said eight apps had it, which was wrong: prints on a 404 and shell -n reads that as present.
|
This pull request now also removes launchpad is the only one of the 21 fleet apps still carrying it. Every other app dropped it in favour of the shared Two gates under one responsibility is worse than either alone. The failing check is named Branch Policy Check while the fleet's is branch-protection / check-branch, so the refusal reads as the fleet gate rejecting a promotion that the fleet gate actually permits. One correction to the commit message: its last line should read " |
Stable release.
betawas 418 commits ahead ofmain.Conflicts were resolved to beta's side, including its refactors: where beta had removed a file, the removal stands rather than resurrecting a stale copy from main. No conflicts.
git merge -X theirssettles content conflicts but leaves modify/delete ones unmerged — beta deleted the file, so there is no "theirs" blob to take. Those were resolved by honouring the deletion.Verified before pushing: the commit has exactly two parents, and no conflicted path was left unresolved. Files main keeps that beta never had (archived openspec docs, whitespace-only differences) are preserved — "beta wins" governs conflicts, not additions.
A failing
… / releasecheck here is the App Store publish step, not a quality gate: 7 apps have no signing key and thematiq's certificate carries its old app id. The GitHub release and tag are still created.