feat(vue3): migrate deskdesk to Vue 3 + nc-vue 2.1.0-vue3.17 (and re-enable this repo's CI) - #76
Conversation
deskdesk was one of four apps missed by the original fleet Vue 3 working set. It
sat on vue@^2.7.14 with @conduction/nextcloud-vue@^1.0.0-beta.101 — the oldest
nc-vue pin in the org.
Dependencies
@conduction/nextcloud-vue ^1.0.0-beta.101 -> 2.1.0-vue3.17 (EXACT, no caret:
a caret DOES float an already-resolved prerelease when the dist-tag moves)
vue ^2.7.14 -> ^3.5.13
@nextcloud/vue ^8.39.0 -> ^9.9.0
@nextcloud/dialogs ^3.2.0 -> ^7.4.1
@nextcloud/router ^3.1.0 (already v3)
vue-router ^3.6.5 -> ^4.6.4
vue-loader ^15 -> ^17.4.2, vue-template-compiler DROPPED
@nextcloud/webpack-vue-config ^6.0.1 -> ^7.0.2, + terser-webpack-plugin
@vue/compiler-sfc added; gridstack ^12 added (nc-vue peer, CSS imported)
vue-apexcharts (Vue-2-only wrapper, ZERO usages in src/) removed;
apexcharts ^3.54.1 -> ^4.7.0, and overrides."vue3-apexcharts" pinned to 1.8.0
because 1.9.0+ is proprietary and forbids sublicensing in our EUPL-1.2 apps.
Bootstrap (src/main.js, src/settings.js, src/router/index.js)
new Vue({...}).$mount() -> createApp({...}).mount()
Vue.mixin / Vue.use -> app.mixin / app.use
PiniaVuePlugin -> app.use(pinia)
new Router({ mode:'history'})-> createRouter({ history: createWebHistory() })
path: '*' -> path: '/:pathMatch(.*)*' (v4 REMOVED the bare
wildcard; it does not error, the route just never matches and <main> is
empty)
Vue.observable() -> reactive()
:open.sync -> v-model:open (CnObjectSidebar really does
declare an `open` prop and emit `update:open` in vue3.17 — checked, not
assumed; the .sync autofix would have written v-model:value)
Mount target renamed #content -> #deskdesk-app in templates/index.php. Vue 2's
$mount() REPLACED the matched element, so mounting on the template's <div
id="content"> silently replaced Nextcloud's own #content wrapper from
layout.user.php. Vue 3's mount() renders INSIDE the match, and with two #content
ids it is undefined which one wins.
The admin settings entry no longer mounts inside the loadTranslations callback.
On installs where /custom_apps/<app>/l10n/<locale>.json 404s the callback never
fires, which is a blank admin panel with no error.
Build config
Aliases now point at ABSOLUTE FILES, not package directories. @nextcloud/vue@9,
@nextcloud/dialogs@7 and vue-router@4 ship an `exports` map with no `main` and
no `module`, and webpack applies an exports map to package requests only —
never to an already-absolutised path — so the old directory aliases resolved to
nothing.
New src/setPublicPath.js, first import of both entry points, sets
__webpack_public_path__ via generateFilePath and __webpack_nonce__. The
hardcoded /apps/<app>/js/ is wrong under custom_apps, and the wrong path does
NOT 404 — Nextcloud answers 200 text/html, so it surfaces as a MIME refusal and
ChunkLoadError. Vue 2 never exposed this because it emitted no async chunks.
sideEffects:true rule for the nc-vue dist, or webpack tree-shakes the
`script.render = render` wrapper and every Cn component renders as a comment
node with no warning.
appName / appVersion DefinePlugin entries re-added — they were being dropped by
replacing webpackConfig.plugins wholesale.
USE_LOCAL_LIB is now opt-IN. It was opt-OUT against ../nextcloud-vue/src, which
is the Vue 2 beta line, so any build from the shared checkout silently compiled
Vue 2 sources into the app.
Lint
eslint.config.js now spreads conductionVue3Fixes LAST. The @nextcloud v8 base
is Vue-2 era and activates ZERO vue/no-deprecated-* rules; verified by
SEVERITY, not by rule name — 21 of 21 now resolve to error/warn.
Fixed pre-existing indentation errors in DetailPageWrapper.vue's watch handler
and declared @SPEC as a known JSDoc tag, as hermiq does.
Verification (npm 11 generate -> npm 10 normalise -> npm 10 ci, all exit 0)
lockfile @conduction/nextcloud-vue = 2.1.0-vue3.17
lockfile vue = 3.5.40, @nextcloud/vue = 9.9.0, vue-router = 4.6.4
lockfile vue3-apexcharts = 1.8.0; bootstrap-vue ABSENT; vue-template-compiler
ABSENT
npm run lint -> 0 errors (was 16), 2 pre-existing jsdoc warnings
npm run stylelint -> exit 0
npm run build -> exit 0, 2 asset-size warnings only
quote-aware multi-line tag scan over all 5 .vue files: ZERO residual .sync,
ZERO <NcButton type=, ZERO plain :value= (positive control: 33 tags matched)
Quality Report — ConductionNL/deskdesk @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| composer | ✅ | ✅ 100/100 | |||
| npm | ✅ | ✅ 651/651 | |||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ |
Coverage: 0% (0/3 statements)
Quality workflow — 2026-08-03 11:35 UTC
Download the full PDF report from the workflow artifacts.
CI is alive again, and this is the first real check run on this repo since 2026-05-26Re-enabling GitHub Actions produced 26 jobs on this PR. Job conclusions read individually, not taken from the PR's overall colour:
21 pass, 2 fail. The two red checks are pre-existing
Please also see the correction to issue #75 in the PR descriptionThe issue's diagnosis ("the workflow produces no runs") was a symptom. The cause was |
What
Migrates deskdesk from Vue 2 to Vue 3 and pins
@conduction/nextcloud-vueto2.1.0-vue3.17(exact — a caret does float an already-resolved prerelease when the dist-tag moves).deskdesk was one of four apps missed by the original fleet Vue 3 working set. It sat on
vue@^2.7.14with@conduction/nextcloud-vue@^1.0.0-beta.101— the oldest nc-vue pin in the org.deskdeskissue #75 reported thatcode-quality.yml"produces no runs at all". The root cause was not the workflow:GitHub Actions was disabled at the repository level. deskdesk was the only one of 8 ConductionNL repos I checked with
enabled: false(nldesign, GDPRdesk, hermiq, docudesk, doriath, decidesk, openregister are alltrue). The last Code Quality run was 2026-05-26 (run 26435406003, 26 jobs), and every push and PR since produced nothing — a gate's failure and its complete absence look identical from the PR page.I re-enabled it (
PUT …/actions/permissions {"enabled": true, "allowed_actions": "all"}), so this PR should be the first deskdesk PR in over two months to get real checks. Please read them rather than assuming green.Note the correction to issue #75 while you are there: the issue says there is "no Code Quality run on
development". There is — run 26435406003 from 2026-05-26, and it did include aSecurity (composer)job, which passed. The gate set exists and works; it was simply switched off.Dependencies
@conduction/nextcloud-vue^1.0.0-beta.1012.1.0-vue3.17exactvue^2.7.14^3.5.13@nextcloud/vue^8.39.0^9.9.0@nextcloud/dialogs^3.2.0^7.4.1vue-router^3.6.5^4.6.4vue-loader^15^17.4.2vue-template-compiler^2.7.16@nextcloud/webpack-vue-config^6.0.1^7.0.2terser-webpack-plugin,@vue/compiler-sfc,gridstack@^12vue-apexcharts(the Vue-2-only wrapper, zero usages insrc/) is removed andapexchartsgoes^3.54.1 → ^4.7.0.overrides."vue3-apexcharts"is pinned to1.8.0because 1.9.0+ is proprietary and forbids sublicensing in our EUPL-1.2 apps. No licence override was added — coreapexchartsis MIT and reports an identicalCustom:symptom.Code
Bootstrap —
new Vue().$mount()→createApp().mount();Vue.mixin/Vue.use→app.mixin/app.use;PiniaVuePlugin→app.use(pinia);Vue.observable()→reactive().Router —
new Router({ mode: 'history' })→createRouter({ history: createWebHistory(...) }), andpath: '*'→path: '/:pathMatch(.*)*'. v4 removed the bare wildcard and does not error: the route simply never matches, the shell renders and<main>stays empty.:open.sync→v-model:openonCnObjectSidebar. Checked against the vue3.17 source rather than assumed — it really does declare anopenprop and emitupdate:open. This is the case whereeslint --fixis dangerous: its.syncautofix preserves the prop name, so it would have writtenv-model:valueon a component with novalueprop — lint green, binding dead. All.syncconversions here were done by hand.Mount target renamed
#content→#deskdesk-appintemplates/index.php. Vue 2's$mount()replaced the matched element, so mounting on the template's<div id="content">quietly replaced Nextcloud's own#contentwrapper fromlayout.user.phpand the duplicate id never showed. Vue 3'smount()renders inside the match, and with two#contentelements it is undefined which one wins.Admin settings no longer mounts inside the
loadTranslationscallback. On installs where/custom_apps/<app>/l10n/<locale>.json404s the callback never fires — a blank admin panel with no error.Build config
@nextcloud/vue@9,@nextcloud/dialogs@7andvue-router@4ship anexportsmap with nomainand nomodule; webpack applies an exports map to package requests and never to an already-absolutised path, so the old Vue-2-era directory aliases resolved to nothing.src/setPublicPath.js, the first import of both entry points, setting__webpack_public_path__viagenerateFilePathand__webpack_nonce__. The hardcoded/apps/<app>/js/is wrong undercustom_apps, and the wrong path does not 404 — Nextcloud answers 200text/html, so it surfaces as a MIME refusal andChunkLoadError. Vue 2 never exposed this because it emitted no async chunks; the Vue 3 dependency set emits many.sideEffects: truerule for the nc-vue dist. The library'ssideEffectsallowlist does not glob-match its compiled*.vue.jswrappers, so webpack tree-shakes thescript.render = renderglue and everyCncomponent renders as a bare comment node with no warning.appName/appVersionDefinePluginentries re-added — they were silently dropped by replacingwebpackConfig.pluginswholesale, which makes every@nextcloud/vuemount log "the library was used without setting the appName".USE_LOCAL_LIBis now opt-IN. It was opt-OUT against../nextcloud-vue/src, which is the Vue 2 beta line — so any build run from the shared apps-extra checkout silently compiled Vue 2 sources into this app.Lint
eslint.config.jsnow spreadsconductionVue3Fixeslast. The@nextcloudv8 base is Vue-2 era and activates zerovue/no-deprecated-*rules, so Vue-2 idioms survive a green lint. Verified by severity, not rule name (--print-configlists rules set toofftoo): 21 of 21 now resolve toerror/warn.Also fixed pre-existing indentation errors in
DetailPageWrapper.vue's watch handler and declared@specas a known JSDoc tag, as hermiq does.Verification
Recipe:
rm -rf package-lock.json node_modules→npm install(npm 11.13.0) →npm install --package-lock-only(npm 10.9.8) →npm ci(npm 10.9.8). All exit 0.npm cipasses without--min-release-age=0, so CI's install is unaffected by the.npmrccooldown.Verified from the lockfile:
@conduction/nextcloud-vue2.1.0-vue3.17vue/@nextcloud/vue/vue-router3.5.40/9.9.0/4.6.4vue3-apexcharts1.8.0(below the proprietary line)bootstrap-vue,vue-template-compilernpm run lintindent,DetailPageWrapper.vue)jsdoc/require-paramwarningsnpm run stylelintnpm run buildA quote-aware multi-line tag scan over all 5
.vuefiles (a line grep undercounts these by 60–100%, and a naive multi-line matcher truncates at the>inside an attribute value) finds zero residual.sync, zero<NcButton type=, zero plain:value=— positive control: 33 tags matched, so the scan can match.Not done
npm run buildis green and the residual scan is clean, but this app has not been loaded in a browser on this branch. It should be before release.test:e2eexists;playwright.config.tsis committed but no suite was executed).openspec/schemas/conduction → ../../../.claude/openspec/schemas/conductionis dangling and escapes the repo. It is untouched here, but it makesdocker cpof this tree refuse outright, so it will block deployment tooling. Separate issue.