Skip to content
Open
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
10,157 changes: 2,895 additions & 7,262 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 13 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,22 @@
"extends @nextcloud/browserslist-config"
],
"dependencies": {
"@conduction/nextcloud-vue": "^0.1.0-beta.3",
"@codemirror/commands": "^6.10.3",
"@conduction/nextcloud-vue": "^0.1.0-beta.7",
"@nextcloud/axios": "^2.5.0",
"@nextcloud/dialogs": "^3.2.0",
"@nextcloud/initial-state": "^2.2.0",
"@nextcloud/l10n": "^2.0.1",
"@nextcloud/router": "^2.0.1",
"@nextcloud/vue": "^8.16.0",
"@nextcloud/l10n": "^3.1.0",
"@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^8.17.0",
"apexcharts": "^3.54.1",
"codemirror": "^6.0.2",
"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-material-design-icons": "^5.3.0",
"vue-router": "^3.6.5"
},
Expand All @@ -46,12 +53,12 @@
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"css-loader": "~7.1.1",
"eslint-plugin-vue": "^9.21.1",
"vue-eslint-parser": "^9.4.3",
"eslint": "^8.56.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-vue": "^9.21.1",
"style-loader": "~4.0.0",
"stylelint": "^15.11.0",
"vue-eslint-parser": "^9.4.3",
"vue-loader": "^15.11.1 <16.0.0",
"vue-template-compiler": "^2.7.16",
"webpack": "^5.94.0",
Expand Down
82 changes: 81 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,33 @@
</NcAppContent>
</template>
<template v-else-if="storesReady && hasOpenRegisters">
<MainMenu />
<MainMenu @open-settings="settingsOpen = true" />
<NcAppContent>
<router-view />
</NcAppContent>
<UserSettings :open="settingsOpen" @update:open="settingsOpen = $event" />
<CnIndexSidebar
v-if="sidebarState.active && !objectSidebarState.active"
:schema="sidebarState.schema"
:visible-columns="sidebarState.visibleColumns"
:search-value="sidebarState.searchValue"
:active-filters="sidebarState.activeFilters"
:facet-data="sidebarState.facetData"
:open="sidebarState.open"
@update:open="sidebarState.open = $event"
@search="onSidebarSearch"
@columns-change="onSidebarColumnsChange"
@filter-change="onSidebarFilterChange" />
<CnObjectSidebar
v-if="objectSidebarState.active"
:object-type="objectSidebarState.objectType"
:object-id="objectSidebarState.objectId"
:title="objectSidebarState.title"
:subtitle="objectSidebarState.subtitle"
:register="objectSidebarState.register"
:schema="objectSidebarState.schema"
:hidden-tabs="objectSidebarState.hiddenTabs"
:open.sync="objectSidebarState.open" />
</template>
<NcAppContent v-else>
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
Expand All @@ -38,11 +61,14 @@
</template>

<script>
import Vue from 'vue'
import { NcButton, NcContent, NcAppContent, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
import { CnIndexSidebar, CnObjectSidebar } from '@conduction/nextcloud-vue'
import { generateUrl, imagePath } from '@nextcloud/router'
import { initializeStores } from './store/store.js'
import { useSettingsStore } from './store/modules/settings.js'
import MainMenu from './navigation/MainMenu.vue'
import UserSettings from './views/settings/UserSettings.vue'

export default {
name: 'App',
Expand All @@ -52,12 +78,46 @@ export default {
NcAppContent,
NcEmptyContent,
NcLoadingIcon,
CnIndexSidebar,
CnObjectSidebar,
MainMenu,
UserSettings,
},

provide() {
return {
sidebarState: this.sidebarState,
objectSidebarState: this.objectSidebarState,
}
},

data() {
return {
storesReady: false,
settingsOpen: false,
objectSidebarState: Vue.observable({
active: false,
open: true,
objectType: '',
objectId: '',
title: '',
subtitle: '',
register: '',
schema: '',
hiddenTabs: [],
}),
sidebarState: Vue.observable({
active: false,
open: true,
schema: null,
visibleColumns: null,
searchValue: '',
activeFilters: {},
facetData: {},
onSearch: null,
onColumnsChange: null,
onFilterChange: null,
}),
}
},

Expand All @@ -82,5 +142,25 @@ export default {
await initializeStores()
this.storesReady = true
},

methods: {
onSidebarSearch(value) {
this.sidebarState.searchValue = value
if (typeof this.sidebarState.onSearch === 'function') {
this.sidebarState.onSearch(value)
}
},
onSidebarColumnsChange(columns) {
this.sidebarState.visibleColumns = columns
if (typeof this.sidebarState.onColumnsChange === 'function') {
this.sidebarState.onColumnsChange(columns)
}
},
onSidebarFilterChange(filter) {
if (typeof this.sidebarState.onFilterChange === 'function') {
this.sidebarState.onFilterChange(filter)
}
},
},
}
</script>
26 changes: 13 additions & 13 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: EUPL-1.2
import Vue from 'vue'
import { PiniaVuePlugin } from 'pinia'
import { translate as t, translatePlural as n, loadTranslations } from '@nextcloud/l10n'
import { translate as t, translatePlural as n } from '@nextcloud/l10n'
import pinia from './pinia.js'
import router from './router/index.js'
import App from './App.vue'
Expand All @@ -16,17 +16,17 @@ import './assets/app.css'
Vue.mixin({ methods: { t, n } })
Vue.use(PiniaVuePlugin)

loadTranslations('app-template', () => {
// Create Vue instance to activate Pinia context, then initialize stores.
const app = new Vue({
pinia,
router,
render: h => h(App),
})
// Create Vue instance to activate Pinia context, then initialize stores.
const app = new Vue({
pinia,
router,
render: h => h(App),
})

// Mount immediately so the App renders (NC32 needs #content to be taken over).
app.$mount('#content')
// Mount immediately — do NOT wrap in loadTranslations() callback as it
// blocks rendering when the l10n JSON file doesn't exist (404).
app.$mount('#content')

// Initialize stores after mount.
initializeStores()
})
// Initialize stores after mount — fire-and-forget so the UI shows immediately.
// The App.vue created() hook also awaits initializeStores() for storesReady flag.
initializeStores()
29 changes: 21 additions & 8 deletions src/navigation/MainMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
<HomeIcon :size="20" />
</template>
</NcAppNavigationItem>
<NcAppNavigationItem
:name="t('app-template', 'Items')"
:to="{ name: 'Items' }">
<template #icon>
<FormatListBulletedIcon :size="20" />
</template>
</NcAppNavigationItem>
<NcAppNavigationItem
:name="t('app-template', 'Documentation')"
@click="openLink('https://conduction.nl', '_blank')">
Expand All @@ -18,30 +25,36 @@
</NcAppNavigationItem>
</template>
<template #footer>
<NcAppNavigationItem
:name="t('app-template', 'Settings')"
:to="{ name: 'Settings' }">
<template #icon>
<CogIcon :size="20" />
</template>
</NcAppNavigationItem>
<NcAppNavigationSettings>
<!-- Add admin/config nav items here (like OpenCatalogi's Catalogs, Themes, etc.) -->
<NcAppNavigationItem
:name="t('app-template', 'Settings')"
@click="$emit('open-settings')">
<template #icon>
<CogIcon :size="20" />
</template>
</NcAppNavigationItem>
</NcAppNavigationSettings>
</template>
</NcAppNavigation>
</template>

<script>
import { NcAppNavigation, NcAppNavigationItem } from '@nextcloud/vue'
import { NcAppNavigation, NcAppNavigationItem, NcAppNavigationSettings } from '@nextcloud/vue'
import BookOpenVariantOutline from 'vue-material-design-icons/BookOpenVariantOutline.vue'
import CogIcon from 'vue-material-design-icons/Cog.vue'
import FormatListBulletedIcon from 'vue-material-design-icons/FormatListBulleted.vue'
import HomeIcon from 'vue-material-design-icons/Home.vue'

export default {
name: 'MainMenu',
components: {
NcAppNavigation,
NcAppNavigationItem,
NcAppNavigationSettings,
BookOpenVariantOutline,
CogIcon,
FormatListBulletedIcon,
HomeIcon,
},
methods: {
Expand Down
15 changes: 12 additions & 3 deletions src/router/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
// SPDX-License-Identifier: EUPL-1.2
//
// Vue Router — always use static imports (no lazy import()).
// Lazy imports cause ChunkLoadError in Nextcloud because chunks are
// served from /apps/<id>/js/ but files live at /custom_apps/<id>/js/.

import Vue from 'vue'
import Router from 'vue-router'
import { generateUrl } from '@nextcloud/router'
import Dashboard from '../views/Dashboard.vue'
import AdminRoot from '../views/settings/AdminRoot.vue'

import ItemList from '../views/items/ItemList.vue'
import ItemDetail from '../views/items/ItemDetail.vue'
Vue.use(Router)

export default new Router({
mode: 'history',
base: generateUrl('/apps/app-template'),
routes: [
{ path: '/', name: 'Dashboard', component: Dashboard },
{ path: '/settings', name: 'Settings', component: AdminRoot },
{ path: '/items', name: 'Items', component: ItemList },
{ path: '/items/:id', name: 'ItemDetail', component: ItemDetail,
props: route => ({ itemId: route.params.id }) },
// No /settings route — settings opens as NcAppSettingsDialog modal
// Admin settings live at /settings/admin/{appid} (managed by NC via AdminSettings.php)
{ path: '*', redirect: '/' },
],
})
73 changes: 14 additions & 59 deletions src/store/modules/object.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,17 @@
// SPDX-License-Identifier: EUPL-1.2
import { defineStore } from 'pinia'
import { getRequestToken } from '@nextcloud/auth'
//
// Shared object store — provides full CRUD for all entity types.
// Do NOT create per-entity stores. Register entity types in store.js,
// then use this store everywhere:
//
// objectStore.registerObjectType('item', schemaId, registerId)
// objectStore.fetchCollection('item', { _limit: 25 })
// objectStore.fetchObject('item', id)
// objectStore.saveObject('item', data)
// objectStore.deleteObject('item', id)
// objectStore.getObject('item', id) // sync getter from cache
// objectStore.loading.item // reactive loading boolean

/**
* Generic OpenRegister object store.
* Configure it with baseUrl and schemaBaseUrl, then register object types.
*/
export const useObjectStore = defineStore('object', {
state: () => ({
baseUrl: '',
schemaBaseUrl: '',
objectTypes: {},
objects: {},
loading: {},
}),
import { createObjectStore } from '@conduction/nextcloud-vue'

actions: {
configure({ baseUrl, schemaBaseUrl }) {
this.baseUrl = baseUrl
this.schemaBaseUrl = schemaBaseUrl
},

registerObjectType(type, schema, register) {
this.objectTypes[type] = { schema, register }
if (!this.objects[type]) {
this.objects[type] = []
}
},

async fetchObjects(type, params = {}) {
if (!this.objectTypes[type]) {
console.warn(`Object type "${type}" is not registered`)
return []
}

this.loading[type] = true
const { schema, register } = this.objectTypes[type]

try {
const url = new URL(this.baseUrl, window.location.origin)
url.searchParams.set('register', register)
url.searchParams.set('schema', schema)
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v))

const response = await fetch(url.toString(), {
headers: { requesttoken: getRequestToken() },
})
if (response.ok) {
const data = await response.json()
this.objects[type] = data.results || data
return this.objects[type]
}
} catch (error) {
console.error(`Failed to fetch ${type} objects:`, error)
} finally {
this.loading[type] = false
}
return []
},
},
})
export const useObjectStore = createObjectStore('object')
Loading
Loading