Bouw een Nextcloud-app op de Conduction-stack — Deel 2: Schema's + manifest
Definieer de desk-, booking- en floor-schema's in OpenRegister, declareer ze in een manifest.json, en zie hoe CnAppRoot ongeveer 200 regels handgeschreven Vue vervangt door drie regels config.
Dit is Deel 2 van de zesdelige DeskDesk-tutorial. Deel 1 liet je achter met een leeg app-skelet: een chassis zonder data. Deel 2 vult dat chassis: drie schema's, een manifest, en dezelfde vijf Cn*-pagina's sturen elke lijst, elke detailweergave en elk dashboard met het schema als enige bron van waarheid.
De vorm waar we steeds van zeggen "dit scheelt je code" krijgt eindelijk getallen: ongeveer 200 regels handgeschreven Vue krimpen tot drie.
Stap 1: Definieer de drie schema's
Open lib/Settings/deskdesk_register.json en vervang het placeholder article-schema door de echte drie. Elk schema is een JSON Schema met een paar OpenRegister-extensies: slug voor de URL-identifier, icon voor de MDI-glyph die de UI gebruikt, en x-openregister-relations voor getypeerde kruisverwijzingen tussen schema's.
"floor": {
"slug": "floor",
"title": "Floor",
"description": "A physical floor in a building.",
"type": "object",
"required": ["label"],
"properties": {
"label": { "type": "string", "example": "Floor 3" },
"building": { "type": "string", "example": "Amsterdam HQ" },
"planImage": { "type": "string", "format": "uri" }
}
}
Het desk-schema voegt de getypeerde relatie naar floor toe:
"desk": {
"slug": "desk",
"title": "Desk",
"type": "object",
"required": ["label", "floor", "zone"],
"properties": {
"label": { "type": "string", "example": "3-East-12" },
"floor": {
"type": "string",
"x-openregister-relations": {
"schema": "floor",
"cardinality": "many-to-one"
}
},
"zone": {
"type": "string",
"enum": ["north", "south", "east", "west", "central"]
},
"equipment": {
"type": "array",
"items": { "type": "string", "enum": ["dual-monitor", "standing", "phonebooth-adjacent", "wired-network", "extra-power"] }
},
"capacity": { "type": "integer", "minimum": 1, "default": 1 },
"accessibility": { "type": "boolean", "default": false },
"photo": { "type": "string", "format": "uri" },
"notes": { "type": "string" }
}
}
Booking heeft dezelfde vorm: een relatie, een paar datetimes, een enum-status en een optionele RRULE voor terugkerende boekingen.
Het volledige bestand staat in de deskdesk repo, met vijf seed-desks, twee floors en drie bookings. Zo is de app meteen te doorbladeren na de eerste install.
Stap 2: Declareer het register zelf
OpenRegister's importhandler maakt schema's gretig aan, maar maakt alleen een register als de JSON er expliciet één declareert. Voeg een components.registers-blok toe boven je schema's:
"components": {
"registers": {
"deskdesk": {
"slug": "deskdesk",
"title": "DeskDesk",
"description": "Floors, desks, and bookings for the DeskDesk app.",
"version": "0.2.0",
"schemas": ["floor", "desk", "booking"]
}
},
"schemas": { /* ... */ },
"objects": { /* ... */ }
}
De schemas-array vertelt OpenRegister welke schema's uit hetzelfde bestand bij dit register horen. De slug wordt de stabiele identifier van het register.
Stap 3: Trigger de import
Twee manieren. Of trigger het via de SettingsController (de app stelt POST /api/settings/load beschikbaar):
curl -X POST -b cookies.txt \
-H "requesttoken: $REQUESTTOKEN" \
http://localhost:8080/index.php/apps/deskdesk/api/settings/load
Of, schoner tijdens lokale ontwikkeling, schakel de app uit en weer in. Dat vuurt de <install>-repair-stap uit appinfo/info.xml:
docker exec nextcloud php occ app:disable deskdesk
docker exec nextcloud php occ app:enable deskdesk
Hoe dan ook, je ziet in de registerlijst van OpenRegister:
slug: deskdesk title: DeskDesk application: deskdesk schemas: floor, desk, booking
Het pad dat de SettingsService neemt is importFromFilePath met een pad dat relatief aan \OC::$SERVERROOT wordt opgelost (dat is /var/www/html in een standaardinstall). De boot-time bug die ik raakte tijdens deze tutorial — Configuration file not found: html/custom_apps/deskdesk/... — was een relatief-pad-fout; de fix staat in de diff.
Stap 4: Het manifest
src/manifest.json is de enige declaratieve beschrijving van de app-shell. Drie top-level keys: dependencies, menu, pages.
{
"$schema": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest.schema.json",
"version": "1.0.0",
"id": "deskdesk",
"title": "DeskDesk",
"dependencies": ["openregister"],
"menu": [
{"id": "desks-index", "label": "deskdesk.menu.desks", "icon": "icon-category-organization", "route": "desks-index", "order": 10},
{"id": "bookings-index", "label": "deskdesk.menu.bookings", "icon": "icon-calendar", "route": "bookings-index", "order": 20},
{"id": "floors-index", "label": "deskdesk.menu.floors", "icon": "icon-category-monitoring", "route": "floors-index", "order": 30}
],
"pages": [
{"id": "desks-index", "route": "/", "type": "index", "title": "deskdesk.desks.indexTitle",
"config": {"register": "deskdesk", "schema": "desk", "columns": ["label","floor","zone","equipment","capacity","accessibility"], "filters": ["floor","zone","equipment","accessibility"], "defaultSort": {"key": "label", "order": "asc"}}},
{"id": "desks-detail", "route": "/desks/:id", "type": "detail", "title": "deskdesk.desks.detailTitle",
"config": {"register": "deskdesk", "schema": "desk"}}
/* bookings + floors pages follow the same pattern */
]
}
Een paar regels om te onthouden:
page.idis de naam van de vue-router-route. CnPageRenderer matcht$route.name === page.idom de juiste pagina te kiezen.page.typeis gesloten.index | detail | dashboard | custom. Gebruikcustommet eencomponent-registry-entry voor eenmalige pagina's.page.configgaat als props door.register,schema,columns,filters,defaultSortmappen één-op-één op de props van CnIndexPage.
Stap 5: Vervang App.vue en de router
Nu het bevredigende deel. Open src/App.vue (de 175-regelige shell uit Deel 1) en vervang hem hierdoor:
<template>
<CnAppRoot :app-id="appId" :manifest="manifest" />
</template>
<script>
import { CnAppRoot } from '@conduction/nextcloud-vue'
import manifest from './manifest.json'
export default {
name: 'App',
components: { CnAppRoot },
data: () => ({ appId: 'deskdesk', manifest }),
}
</script>
Dat is alles. De <NcContent>, de dependency-check empty-state, de loading-spinner, het menu, de sidebars, de user-settings-dialog: CnAppRoot levert ze allemaal en zet ze achter de dependencies-array uit het manifest.
Dan wordt src/router/index.js een mapping van manifest naar routes:
import Vue from 'vue'
import Router 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: '/' })
export default new Router({
mode: 'history',
base: generateUrl('/apps/deskdesk'),
routes,
})
Elke pagina in het manifest krijgt een route. Elke route rendert CnPageRenderer. CnPageRenderer leest het manifest, vindt de matchende pagina-entry op routenaam, en dispatcht op page.type:
type: "index"→ CnIndexPage met de kolommen en filters van het schematype: "detail"→ CnDetailPage met de properties van het schematype: "dashboard"→ CnDashboardPage met de layouttype: "custom"→ je geregistreerde component
Stap 6: Verwijder de oude views
Het hele punt van Deel 2 is dat het framework de views voor je schrijft. Verwijder:
src/navigation/MainMenu.vue— vervangen door CnAppNav (auto-mounted door CnAppRoot)src/views/Dashboard.vue— placeholder; komt later terug in een rijkere vorm wanneer de dashboardpagina dat nodig heeftsrc/views/items/ItemList.vue,src/views/items/ItemDetail.vue— vervangen door CnIndexPage + CnDetailPagesrc/views/settings/UserSettings.vue,src/views/settings/Settings.vue— instellingen leven op de admin-pagina (AdminRoot.vue) en de in-app-dialog komt mee met CnAppRoot
Het voor-en-na op schijf:
before: 12 .vue files in src/, ~600 lines of hand-rolled component code
after: 3 .vue files (App.vue, AdminRoot.vue, that's it), ~30 lines
Stap 7: Bouw, herlaad, blader
npm run build
docker cp ./js nextcloud:/var/www/html/custom_apps/deskdesk/
docker exec nextcloud apache2ctl graceful
Open /apps/deskdesk/. De linker rail leest nu Desks · Bookings · Floors (de vertaalkeys zien er nog uit als deskdesk.menu.desks totdat je l10n/-entries toevoegt; dat is een follow-up). De hoofdkolom toont CnIndexPage met de kolommen uit het schema. Klik een rij aan, je belandt op CnDetailPage voor die desk. De hele CRUD-flow krijg je cadeau.
Waarom dit belangrijk is
Je schreef drie JSON-bestanden. Het framework leest ze en rendert de app. Er is geen DesksList.vue, geen DeskForm.vue, geen useDesks.js. Als je een priority-veld toevoegt aan het booking-schema, pikt elke plek waar een booking verschijnt — tabelkolom, detailrij, filter-sidebar, formulierdialoog — het nieuwe veld automatisch op.
Dit is de schema-gedreven discipline waarover nextcloud-vue.conduction.nl praat. Deel 3 zet het op een manier in die mensen de eerste keer verrast: één JSON-blok op het booking-schema laat elke booking in de Nextcloud-agenda van de gebruiker verschijnen. Geen controller, geen event, geen listener.
