Skip to content
This repository was archived by the owner on Aug 4, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ const {
FlatCompat,
} = require('@eslint/eslintrc')

// The `@nextcloud` v8 base is Vue-2 era: on its own it activates ZERO
// `vue/no-deprecated-*` rules, so Vue-2 idioms (`beforeDestroy`, `.sync`,
// `filters:`) survive a green lint. `conductionVue3Fixes` layers the Vue 3
// rules on top and must be spread LAST so it wins. It registers no plugins,
// which is why it layers cleanly onto the @nextcloud base.
//
// CJS: the extensionless subpath works because the package ships no `exports`
// map. From ESM this would need `/eslint/index.js`.
const {
conductionVue3Fixes,
} = require('@conduction/nextcloud-vue/eslint')

const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
Expand All @@ -34,6 +46,10 @@ module.exports = defineConfig([{
// Allow unused i18n functions (t, n) — imported for future translation wiring
'no-unused-vars': ['error', { varsIgnorePattern: '^(t|n)$', argsIgnorePattern: '^_' }],
'jsdoc/require-jsdoc': 'off',
// @spec is the Conduction OpenSpec traceability tag (gate-16); it is a
// deliberate, org-wide custom JSDoc tag, not a typo. Same declaration as
// hermiq's config.
'jsdoc/check-tag-names': ['warn', { definedTags: ['spec'] }],
'vue/first-attribute-linebreak': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'n/no-missing-import': 'off',
Expand All @@ -43,4 +59,4 @@ module.exports = defineConfig([{
'import/no-named-as-default': 'off', // disable named-as-default checking to avoid parser requirement
'import/no-named-as-default-member': 'off', // disable named-as-default-member checking to avoid parser requirement
},
}])
}, ...conductionVue3Fixes])
3,916 changes: 2,175 additions & 1,741 deletions package-lock.json

Large diffs are not rendered by default.

26 changes: 14 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,33 +22,34 @@
"extends @nextcloud/browserslist-config"
],
"dependencies": {
"@babel/core": "^7.29.0",
"@codemirror/commands": "^6.10.3",
"@conduction/nextcloud-vue": "^1.0.0-beta.101",
"@conduction/nextcloud-vue": "2.1.0-vue3.17",
"@nextcloud/auth": "^2.5.0",
"@nextcloud/axios": "~2.5.2",
"@nextcloud/dialogs": "^3.2.0",
"@nextcloud/dialogs": "^7.4.1",
"@nextcloud/initial-state": "^2.2.0",
"@nextcloud/l10n": "3.4.1",
"@nextcloud/router": "^3.1.0",
"@nextcloud/vue": "^8.39.0",
"apexcharts": "^3.54.1",
"@nextcloud/vue": "^9.9.0",
"apexcharts": "^4.7.0",
"codemirror": "^6.0.2",
"gridstack": "^12.0.0",
"node-polyfill-webpack-plugin": "4.0.0",
"pinia": "^2.1.7",
"sass": "^1.77.0",
"sass-loader": "^16.0.0",
"vue": "^2.7.14",
"vue-apexcharts": "^1.7.0",
"vue": "^3.5.13",
"vue-material-design-icons": "^5.3.0",
"vue-router": "^3.6.5"
"vue-router": "^4.6.4"
},
"overrides": {
"libxmljs2": "^0.37.0",
"@nextcloud/axios": "~2.5.2",
"@nextcloud/l10n": "3.4.1"
"@nextcloud/l10n": "3.4.1",
"vue3-apexcharts": "1.8.0"
},
"devDependencies": {
"@babel/core": "^7.29.0",
"@babel/eslint-parser": "^7.28.6",
"@cyclonedx/cyclonedx-npm": "^4.2.1",
"@eslint/config-helpers": "^0.4.2",
Expand All @@ -58,10 +59,11 @@
"@nextcloud/eslint-config": "^8.4.1",
"@nextcloud/eslint-plugin": "^2.2.1",
"@nextcloud/stylelint-config": "^2.4.0",
"@nextcloud/webpack-vue-config": "^6.0.1",
"@nextcloud/webpack-vue-config": "^7.0.2",
"@playwright/test": "^1.49.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vue/compiler-sfc": "^3.5.13",
"@vue/eslint-config-typescript": "^13.0.0",
"babel-loader": "^10.1.1",
"css-loader": "~7.1.1",
Expand All @@ -79,10 +81,10 @@
"stylelint": "^15.11.0",
"stylelint-config-recommended-scss": "^13.1.0",
"stylelint-config-recommended-vue": "^1.6.1",
"terser-webpack-plugin": "^5.6.1",
"typescript": "^5.9.3",
"vue-eslint-parser": "^9.4.3",
"vue-loader": "^15.11.1 <16.0.0",
"vue-template-compiler": "^2.7.16",
"vue-loader": "^17.4.2",
"webpack": "^5.94.0",
"webpack-cli": "^6.0.1"
}
Expand Down
13 changes: 9 additions & 4 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<template #sidebar>
<CnObjectSidebar
v-if="objectSidebarState.active"
v-model:open="objectSidebarState.open"
:object-type="objectSidebarState.objectType"
:object-id="objectSidebarState.objectId"
:title="objectSidebarState.title"
Expand All @@ -16,14 +17,13 @@
:schema="objectSidebarState.schema"
:tabs="objectSidebarState.tabs"
:hidden-tabs="objectSidebarState.hiddenTabs"
:custom-components="customComponents"
:open.sync="objectSidebarState.open" />
:custom-components="customComponents" />
</template>
</CnAppRoot>
</template>

<script>
import Vue from 'vue'
import { reactive } from 'vue'
import { CnAppRoot, CnObjectSidebar, defaultPageTypes } from '@conduction/nextcloud-vue'
import manifest from './manifest.json'
import IndexPageWrapper from './views/IndexPageWrapper.vue'
Expand All @@ -34,6 +34,8 @@ import KnowledgeTab from './views/KnowledgeTab.vue'
// that bridge manifest config + route params onto CnIndexPage and
// CnDetailPage. See {Index,Detail}PageWrapper.vue for the rationale.
// Other page types (`dashboard`, `settings`, ...) keep library defaults.
// `defaultPageTypes` is exported FROZEN, so it is spread into a fresh object
// before the two overrides are layered on — mutating the export in place throws.
const pageTypes = {
...defaultPageTypes,
index: IndexPageWrapper,
Expand Down Expand Up @@ -71,7 +73,10 @@ export default {
manifest,
pageTypes,
customComponents,
objectSidebarState: Vue.observable({
// `Vue.observable()` was removed in Vue 3 — `reactive()` is the
// replacement. This object is `provide()`d, so it must stay the same
// identity across the component's lifetime.
objectSidebarState: reactive({
active: false,
open: true,
objectType: '',
Expand Down
41 changes: 31 additions & 10 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,53 @@
// SPDX-License-Identifier: EUPL-1.2
import Vue from 'vue'
import { PiniaVuePlugin } from 'pinia'

// Must stay first: sets __webpack_public_path__ / __webpack_nonce__ before any
// CSS, asset URL or lazy chunk URL is evaluated. See setPublicPath.js.
import './setPublicPath.js'
import { createApp, h } from 'vue'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import { registerBuiltinDashboardWidgets } from '@conduction/nextcloud-vue'
import pinia from './pinia.js'
import router from './router/index.js'
import App from './App.vue'
import { initializeStores } from './store/store.js'

// Library CSS — must be explicit import (webpack tree-shakes side-effect imports from aliased packages)
import '@conduction/nextcloud-vue/css/index.css'
// gridstack is an nc-vue peerDependency the library deliberately does NOT bundle,
// stylesheet included. Without it v12 sizes items with an undefined
// `--gs-column-width`, so every dashboard item renders 0 px wide with NO error.
import 'gridstack/dist/gridstack.min.css'

// Global (unscoped) app styles
import './assets/app.css'

Vue.mixin({ methods: { t, n } })
Vue.use(PiniaVuePlugin)
// nc-vue marks itself `sideEffects: ["**/*.css"]`, so webpack is free to drop the
// bare imports that register the built-in `stat` / `object-table` dashboard
// widgets — they then render "Widget not available" with no error. An explicit
// call is something webpack cannot tree-shake.
registerBuiltinDashboardWidgets()

// Create Vue instance to activate Pinia context. We mount AFTER
// Create the app instance to activate the Pinia context. We mount AFTER
// initializeStores() so the manifest-driven IndexPageWrapper finds the
// object-store types registered when its mounted() hook fires. Without
// this await, IndexPageWrapper.mounted() races initializeStores() and
// CnIndexPage shows "No items found" on first paint.
const app = new Vue({
pinia,
router,
render: h => h(App),
//
// ⚠️ Mount target is `#deskdesk-app`, NOT `#content`. Vue 2's `$mount()`
// REPLACED the matched element, so mounting on templates/index.php's
// `<div id="content">` quietly replaced Nextcloud's own `#content` wrapper from
// layout.user.php and the duplicate id never showed. Vue 3's `mount()` renders
// INSIDE the match, so the app would end up nested in core's wrapper — and with
// two `#content` elements it is undefined which one is matched. A dedicated host
// id removes the ambiguity entirely.
const app = createApp({
render: () => h(App),
})

app.mixin({ methods: { t, n } })
app.use(pinia)
app.use(router)

initializeStores().finally(() => {
app.$mount('#content')
app.mount('#deskdesk-app')
})
17 changes: 8 additions & 9 deletions src/router/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: EUPL-1.2
//
// Vue Router — manifest-driven.
// Vue Router 4 — manifest-driven.
//
// Every entry in manifest.pages becomes a route whose `name` matches
// `page.id` (so CnPageRenderer can dispatch the correct stacked view)
Expand All @@ -9,24 +9,23 @@
// and chooses CnIndexPage / CnDetailPage / CnDashboardPage / a custom
// component based on `page.type`.

import Vue from 'vue'
import Router from 'vue-router'
import { createRouter, createWebHistory } from 'vue-router'
import { generateUrl } from '@nextcloud/router'
import { CnPageRenderer } from '@conduction/nextcloud-vue'
import manifest from '../manifest.json'

Vue.use(Router)

const routes = manifest.pages.map((page) => ({
name: page.id,
path: page.route,
component: CnPageRenderer,
}))

routes.push({ path: '*', redirect: '/' })
// vue-router 4 REMOVED the bare `path: '*'` wildcard. It does not error — the
// route silently never matches, so the shell renders and `<main>` stays empty.
// The v4 spelling is a named catch-all param.
routes.push({ path: '/:pathMatch(.*)*', redirect: '/' })

export default new Router({
mode: 'history',
base: generateUrl('/apps/deskdesk'),
export default createRouter({
history: createWebHistory(generateUrl('/apps/deskdesk')),
routes,
})
28 changes: 28 additions & 0 deletions src/setPublicPath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* eslint-disable camelcase, no-undef */
// SPDX-License-Identifier: EUPL-1.2
/**
* Webpack runtime bootstrap — MUST be the first import of every entry point.
*
* `@nextcloud/webpack-vue-config` hardcodes `output.publicPath` to
* `/apps/deskdesk/js/`. DeskDesk is installed under a non-default apps path
* (`apps-extra` / `custom_apps`), so the real webroot differs — and the wrong
* path does NOT 404: Nextcloud answers 200 with `text/html`, which surfaces as
* a MIME refusal and `ChunkLoadError` rather than a missing-file error.
*
* Vue 2 never exposed this because the old bundle emitted no async chunks. The
* Vue 3 dependency set (@nextcloud/dialogs@7, @nextcloud/files, @mdi/js) splits
* into many, and only the routes that touch them break — the entry bundle looks
* clean. `generateFilePath` resolves the correct path at runtime.
*
* This has to run BEFORE the entry's CSS imports: `asset/resource` URLs are
* computed as `__webpack_require__.p + '<hash>'` when the importing CSS module
* evaluates, and ES imports evaluate before the entry body's statements — so a
* dedicated first-imported module is the only ordering that is early enough.
*
* `__webpack_nonce__` carries Nextcloud's CSP nonce onto any dynamically
* injected chunk, which strict CSP would otherwise block.
*/
import { generateFilePath } from '@nextcloud/router'

__webpack_nonce__ = btoa(OC.requestToken)
__webpack_public_path__ = generateFilePath('deskdesk', '', 'js/')
38 changes: 28 additions & 10 deletions src/settings.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
// SPDX-License-Identifier: EUPL-1.2
import Vue from 'vue'
import { PiniaVuePlugin } from 'pinia'

// Must stay first: sets __webpack_public_path__ / __webpack_nonce__ — see setPublicPath.js.
import './setPublicPath.js'
import { createApp, h } from 'vue'
import { translate as t, translatePlural as n, loadTranslations } from '@nextcloud/l10n'
import pinia from './pinia.js'
import AdminRoot from './views/settings/AdminRoot.vue'

Vue.mixin({ methods: { t, n } })
Vue.use(PiniaVuePlugin)

loadTranslations('deskdesk', () => {
new Vue({
pinia,
render: h => h(AdminRoot),
}).$mount('#deskdesk-settings')
// The mount used to sit INSIDE the loadTranslations callback. Some Nextcloud
// installs only allow the JS/CSS allowlist through Apache, so
// /custom_apps/deskdesk/l10n/<locale>.json can 404 — and the callback then never
// fires, leaving a blank admin panel with no error. Mount unconditionally and
// let translations arrive (or not) on their own; strings fall back to the
// English source either way.
const app = createApp({
render: () => h(AdminRoot),
})

app.mixin({ methods: { t, n } })
app.use(pinia)

// Vue 3 renders INSIDE the matched element rather than replacing it, so the
// `#deskdesk-settings` div from templates/settings/admin.php is preserved.
app.mount('#deskdesk-settings')

try {
const result = loadTranslations('deskdesk', () => {})
if (result && typeof result.then === 'function') {
result.then(() => {}, () => {})
}
} catch {
// Non-fatal — strings fall back to the English source.
}
30 changes: 15 additions & 15 deletions src/views/DetailPageWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,22 @@ export default {
* @spec exclude academy tutorial demo — fetches the detail object when the route id changes, no spec-worthy behavior
*/
async handler() {
if (!this.id) return
// Capture the target ID to guard against races when the user
// navigates quickly between detail pages (issue #59).
const targetId = this.id
this.loading = true
try {
const obj = await this.objectStore.fetchObject(this.schema, targetId)
if (this.id === targetId) {
this.object = obj
if (!this.id) return
// Capture the target ID to guard against races when the user
// navigates quickly between detail pages (issue #59).
const targetId = this.id
this.loading = true
try {
const obj = await this.objectStore.fetchObject(this.schema, targetId)
if (this.id === targetId) {
this.object = obj
}
} finally {
if (this.id === targetId) {
this.loading = false
}
}
} finally {
if (this.id === targetId) {
this.loading = false
}
}
},
},
},
},
}
Expand Down
2 changes: 1 addition & 1 deletion templates/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
$appId = OCA\DeskDesk\AppInfo\Application::APP_ID;
Util::addScript($appId, $appId . '-main');
?>
<div id="content"></div>
<div id="deskdesk-app"></div>
Loading
Loading