diff --git a/lib/Service/SettingsService.php b/lib/Service/SettingsService.php index 81714dd..e19f5e4 100644 --- a/lib/Service/SettingsService.php +++ b/lib/Service/SettingsService.php @@ -153,15 +153,36 @@ public function loadConfiguration(bool $force=false): array } try { + // Resolve the bundled register file relative to the Nextcloud root, + // matching ConfigurationService::importFromFilePath's expectation + // (path relative to /var/www/html). + $appPath = (string) $this->appManager->getAppPath(Application::APP_ID); + $absolute = $appPath.'/lib/Settings/deskdesk_register.json'; + $ncRoot = \OC::$SERVERROOT; + $relative = (str_starts_with($absolute, $ncRoot.'/') === true) + ? substr($absolute, strlen($ncRoot) + 1) + : ltrim($absolute, '/'); + + $version = '0.2.0'; + if (file_exists($absolute) === true) { + $payload = json_decode((string) file_get_contents($absolute), true); + $version = (string) ($payload['info']['version'] ?? $version); + } + $configurationService = $this->container->get('OCA\OpenRegister\Service\ConfigurationService'); - $result = $configurationService->importFromApp(appId: Application::APP_ID, force: $force); + $result = $configurationService->importFromFilePath( + appId: Application::APP_ID, + filePath: $relative, + version: $version, + force: $force + ); if (empty($result) === false) { $this->logger->info('DeskDesk: register configuration imported successfully'); return [ 'success' => true, 'message' => 'Configuration imported successfully.', - 'version' => ($result['version'] ?? 'unknown'), + 'version' => $version, ]; } diff --git a/lib/Settings/deskdesk_register.json b/lib/Settings/deskdesk_register.json index 2f64d14..52b1358 100644 --- a/lib/Settings/deskdesk_register.json +++ b/lib/Settings/deskdesk_register.json @@ -2,8 +2,8 @@ "openapi": "3.0.0", "info": { "title": "DeskDesk Register", - "description": "Register containing all schemas for the DeskDesk application — desks, bookings, and floors.", - "version": "0.1.0" + "description": "Register containing all schemas for the DeskDesk application — floors, desks, and bookings.", + "version": "0.2.0" }, "x-openregister": { "type": "application", @@ -13,84 +13,273 @@ }, "paths": {}, "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": { - "article": { - "slug": "article", - "icon": "FileDocumentOutline", + "floor": { + "slug": "floor", + "icon": "OfficeBuildingOutline", "version": "0.1.0", - "title": "Article", - "description": "schema.org/Article — a written composition published in print or digital form. Aligned with https://schema.org/Article per ADR-011: use schema.org vocabulary instead of ad-hoc domain types whenever a standard type fits.", + "title": "Floor", + "description": "A physical floor in a building. Group desks together by location so users can browse and filter by where they sit.", "type": "object", - "x-schema-org": "https://schema.org/Article", - "required": [ - "name" - ], + "required": ["label"], "properties": { - "name": { + "label": { "type": "string", - "description": "The name/headline of the article (schema.org/name).", - "example": "Getting started with the app template" + "description": "Display name of the floor.", + "example": "Floor 3" }, - "description": { + "building": { "type": "string", - "description": "A short description of the article (schema.org/description).", - "example": "A worked example showing how to replace this schema with your own." + "description": "Optional building identifier when an organisation occupies more than one.", + "example": "Amsterdam HQ" }, - "identifier": { + "planImage": { "type": "string", - "description": "An external identifier for the article — ISBN, DOI, URI, etc. (schema.org/identifier).", - "example": "doi:10.1000/example" + "format": "uri", + "description": "URL to a static floor-plan image, rendered next to the floor in the detail view.", + "example": "https://example.com/floor-plans/floor-3.svg" + } + } + }, + "desk": { + "slug": "desk", + "icon": "DeskOutline", + "version": "0.1.0", + "title": "Desk", + "description": "A bookable desk in an open-office. Desks belong to a floor and a zone, and carry equipment + accessibility metadata so users can pick the right one.", + "type": "object", + "required": ["label", "floor", "zone"], + "properties": { + "label": { + "type": "string", + "description": "Short, unique label for the desk. Convention: floor + zone + sequence (e.g. 3-East-12).", + "example": "3-East-12" + }, + "floor": { + "type": "string", + "description": "Floor this desk belongs to. References a floor object by uuid.", + "x-openregister-relations": { + "schema": "floor", + "cardinality": "many-to-one" + }, + "example": "9b6d8d8f-1a8a-4c5d-9d5e-6e3a8a9c9d12" + }, + "zone": { + "type": "string", + "description": "Zone within the floor. Helps users find desks near specific facilities (printer, kitchen, phonebooths).", + "enum": ["north", "south", "east", "west", "central"], + "example": "east" + }, + "equipment": { + "type": "array", + "description": "Equipment available at the desk. Used as a filter in the index page.", + "items": { + "type": "string", + "enum": ["dual-monitor", "standing", "phonebooth-adjacent", "wired-network", "extra-power"] + }, + "example": ["dual-monitor", "wired-network"] + }, + "capacity": { + "type": "integer", + "minimum": 1, + "default": 1, + "description": "How many people the desk seats. Most desks seat one; meeting desks can seat more.", + "example": 1 }, - "dateCreated": { + "accessibility": { + "type": "boolean", + "default": false, + "description": "True when the desk is wheelchair-accessible (height-adjustable, clear floor space, accessible route).", + "example": false + }, + "photo": { "type": "string", - "format": "date", - "description": "The date the article was created (schema.org/dateCreated).", - "example": "2026-04-20" + "format": "uri", + "description": "URL to a photo of the desk in context.", + "example": "https://example.com/desk-photos/3-east-12.jpg" }, - "author": { + "notes": { "type": "string", - "description": "The author of the article (schema.org/author). Kept as a string for template simplicity; nest a Person object in real apps.", - "example": "Conduction B.V." + "description": "Free-form notes for the desk (quirks, etiquette, neighbours).", + "example": "Window seat. Direct sun in the afternoon — bring a curtain clip if you need shade." + } + } + }, + "booking": { + "slug": "booking", + "icon": "CalendarClock", + "version": "0.1.0", + "title": "Booking", + "description": "A reservation of one desk by one user for a time slot. Status moves tentative -> confirmed -> cancelled. Recurrence is optional.", + "type": "object", + "required": ["desk", "user", "start", "end"], + "properties": { + "desk": { + "type": "string", + "description": "Desk being booked. References a desk object by uuid.", + "x-openregister-relations": { + "schema": "desk", + "cardinality": "many-to-one" + }, + "example": "8a8a8a8a-1234-1234-1234-123412341234" + }, + "user": { + "type": "string", + "description": "Nextcloud user id who made the booking.", + "example": "alice" + }, + "start": { + "type": "string", + "format": "date-time", + "description": "Start of the booking. ISO 8601 with timezone.", + "example": "2026-05-12T09:00:00+02:00" + }, + "end": { + "type": "string", + "format": "date-time", + "description": "End of the booking. ISO 8601 with timezone.", + "example": "2026-05-12T17:00:00+02:00" + }, + "purpose": { + "type": "string", + "description": "Short note about why the desk was booked. Useful for teams sharing a zone.", + "example": "Focus work — review documents" + }, + "status": { + "type": "string", + "enum": ["tentative", "confirmed", "cancelled"], + "default": "confirmed", + "description": "Lifecycle state of the booking.", + "example": "confirmed" + }, + "recurrence": { + "type": "string", + "description": "Optional RFC 5545 RRULE for recurring bookings. Keeps the schema simple; the recurrence engine can expand on read.", + "example": "FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12" } } } }, "objects": { - "article-gemeente-utrecht": { + "floor-3-amsterdam": { + "@self": { + "register": "deskdesk", + "schema": "floor", + "slug": "floor-3-amsterdam" + }, + "label": "Floor 3", + "building": "Amsterdam HQ" + }, + "floor-2-amsterdam": { + "@self": { + "register": "deskdesk", + "schema": "floor", + "slug": "floor-2-amsterdam" + }, + "label": "Floor 2", + "building": "Amsterdam HQ" + }, + "desk-3-east-12": { + "@self": { + "register": "deskdesk", + "schema": "desk", + "slug": "desk-3-east-12" + }, + "label": "3-East-12", + "floor": "floor-3-amsterdam", + "zone": "east", + "equipment": ["dual-monitor", "wired-network"], + "capacity": 1, + "accessibility": false, + "notes": "Window seat with direct afternoon sun." + }, + "desk-3-east-13": { + "@self": { + "register": "deskdesk", + "schema": "desk", + "slug": "desk-3-east-13" + }, + "label": "3-East-13", + "floor": "floor-3-amsterdam", + "zone": "east", + "equipment": ["dual-monitor", "standing"], + "capacity": 1, + "accessibility": true + }, + "desk-3-central-meeting": { + "@self": { + "register": "deskdesk", + "schema": "desk", + "slug": "desk-3-central-meeting" + }, + "label": "3-Central-Meeting", + "floor": "floor-3-amsterdam", + "zone": "central", + "equipment": ["phonebooth-adjacent", "extra-power"], + "capacity": 4, + "accessibility": true, + "notes": "Four-person collaboration desk near the phonebooths." + }, + "desk-2-west-04": { + "@self": { + "register": "deskdesk", + "schema": "desk", + "slug": "desk-2-west-04" + }, + "label": "2-West-04", + "floor": "floor-2-amsterdam", + "zone": "west", + "equipment": ["wired-network"], + "capacity": 1, + "accessibility": false + }, + "booking-admin-3-east-12-mon": { "@self": { "register": "deskdesk", - "schema": "article", - "slug": "article-gemeente-utrecht" + "schema": "booking", + "slug": "booking-admin-3-east-12-mon" }, - "name": "Gemeente Utrecht pilot", - "description": "Seed article describing a realistic pilot project for a Dutch municipality. Per ADR-001 every app MUST include 3-5 realistic objects per schema so the app is immediately demonstrable after install.", - "identifier": "pilot-2026-utrecht", - "dateCreated": "2026-03-15", - "author": "Conduction B.V." + "desk": "desk-3-east-12", + "user": "admin", + "start": "2026-05-12T09:00:00+02:00", + "end": "2026-05-12T17:00:00+02:00", + "purpose": "Focus work — code review", + "status": "confirmed" }, - "article-conduction-consultancy": { + "booking-admin-meeting-3-central": { "@self": { "register": "deskdesk", - "schema": "article", - "slug": "article-conduction-consultancy" + "schema": "booking", + "slug": "booking-admin-meeting-3-central" }, - "name": "Conduction consultancy engagement", - "description": "Seed article covering architecture review and ADR authoring for a consultancy engagement. Values are fictional but realistic.", - "identifier": "engagement-2026-consultancy", - "dateCreated": "2026-03-22", - "author": "Conduction B.V." + "desk": "desk-3-central-meeting", + "user": "admin", + "start": "2026-05-13T13:00:00+02:00", + "end": "2026-05-13T15:00:00+02:00", + "purpose": "Sprint planning", + "status": "confirmed" }, - "article-travel-agency": { + "booking-tentative-2-west": { "@self": { "register": "deskdesk", - "schema": "article", - "slug": "article-travel-agency" + "schema": "booking", + "slug": "booking-tentative-2-west" }, - "name": "Nieuw Horizon travel agency onboarding", - "description": "Third seed article demonstrating variety of content. Re-importing is idempotent: ImportHandler matches by slug.", - "identifier": "onboarding-2026-nieuwhorizon", - "dateCreated": "2026-04-01", - "author": "Conduction B.V." + "desk": "desk-2-west-04", + "user": "admin", + "start": "2026-05-14T09:00:00+02:00", + "end": "2026-05-14T13:00:00+02:00", + "purpose": "Quiet morning", + "status": "tentative" } } } diff --git a/src/App.vue b/src/App.vue index 0f2394c..c44450a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,175 +1,22 @@ diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..ca10172 --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,100 @@ +{ + "$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" + } + }, + { + "id": "bookings-index", + "route": "/bookings", + "type": "index", + "title": "deskdesk.bookings.indexTitle", + "config": { + "register": "deskdesk", + "schema": "booking", + "columns": ["desk", "user", "start", "end", "status", "purpose"], + "filters": ["status", "user"], + "defaultSort": {"key": "start", "order": "desc"} + } + }, + { + "id": "bookings-detail", + "route": "/bookings/:id", + "type": "detail", + "title": "deskdesk.bookings.detailTitle", + "config": { + "register": "deskdesk", + "schema": "booking" + } + }, + { + "id": "floors-index", + "route": "/floors", + "type": "index", + "title": "deskdesk.floors.indexTitle", + "config": { + "register": "deskdesk", + "schema": "floor", + "columns": ["label", "building"], + "defaultSort": {"key": "label", "order": "asc"} + } + }, + { + "id": "floors-detail", + "route": "/floors/:id", + "type": "detail", + "title": "deskdesk.floors.detailTitle", + "config": { + "register": "deskdesk", + "schema": "floor" + } + } + ] +} diff --git a/src/navigation/MainMenu.vue b/src/navigation/MainMenu.vue deleted file mode 100644 index 0320edd..0000000 --- a/src/navigation/MainMenu.vue +++ /dev/null @@ -1,67 +0,0 @@ - - - diff --git a/src/router/index.js b/src/router/index.js index 90eadfa..8395e9c 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,27 +1,32 @@ // 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//js/ but files live at /custom_apps//js/. +// Vue Router — manifest-driven. +// +// Every entry in manifest.pages becomes a route whose `name` matches +// `page.id` (so CnPageRenderer can dispatch the correct stacked view) +// and whose `path` is `page.route`. The component for every page is +// CnPageRenderer; it reads the matched page from the injected manifest +// and chooses CnIndexPage / CnDetailPage / CnDashboardPage / a custom +// component based on `page.type`. import Vue from 'vue' import Router from 'vue-router' import { generateUrl } from '@nextcloud/router' -import Dashboard from '../views/Dashboard.vue' -import ItemList from '../views/items/ItemList.vue' -import ItemDetail from '../views/items/ItemDetail.vue' +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: [ - { path: '/', name: 'Dashboard', component: Dashboard }, - { 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: '/' }, - ], + routes, }) diff --git a/src/store/store.js b/src/store/store.js index cebe9ec..a020475 100644 --- a/src/store/store.js +++ b/src/store/store.js @@ -1,29 +1,27 @@ // SPDX-License-Identifier: EUPL-1.2 // // Store initialisation — called once from main.js after Vue mounts. -// Fetches settings, then registers each entity type with the object store. -// Add one registerObjectType() call per entity your app manages. +// Fetches settings, then registers each DeskDesk entity type with the +// shared object store so CnIndexPage / CnDetailPage can fetch them by +// slug. The register id is `deskdesk` (matching in info.xml and +// the x-openregister.app field in lib/Settings/deskdesk_register.json). import { useObjectStore } from './modules/object.js' import { useSettingsStore } from './modules/settings.js' +const REGISTER = 'deskdesk' +const SCHEMAS = ['floor', 'desk', 'booking'] + export async function initializeStores() { const settingsStore = useSettingsStore() const objectStore = useObjectStore() const config = await settingsStore.fetchSettings() - if (config) { - // Register each entity type: (typeName, schemaId, registerId) - // The schema and register IDs come from the settings API. - objectStore.registerObjectType( - 'item', - config.item_schema || 'item', - config.register || 'deskdesk', - ) - - // Add more entity types here as your app grows: - // objectStore.registerObjectType('category', config.category_schema || 'category', config.register || 'deskdesk') + const registerId = (config && config.register) || REGISTER + + for (const schema of SCHEMAS) { + objectStore.registerObjectType(schema, schema, registerId) } return { settingsStore, objectStore } diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue deleted file mode 100644 index 03ab0e9..0000000 --- a/src/views/Dashboard.vue +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - diff --git a/src/views/items/ItemDetail.vue b/src/views/items/ItemDetail.vue deleted file mode 100644 index 45b3a9a..0000000 --- a/src/views/items/ItemDetail.vue +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - diff --git a/src/views/items/ItemList.vue b/src/views/items/ItemList.vue deleted file mode 100644 index 64b04cd..0000000 --- a/src/views/items/ItemList.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - - diff --git a/src/views/settings/AdminRoot.vue b/src/views/settings/AdminRoot.vue index de76d0f..4a4faab 100644 --- a/src/views/settings/AdminRoot.vue +++ b/src/views/settings/AdminRoot.vue @@ -2,12 +2,12 @@ diff --git a/src/views/settings/Settings.vue b/src/views/settings/Settings.vue deleted file mode 100644 index d87b899..0000000 --- a/src/views/settings/Settings.vue +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - diff --git a/src/views/settings/UserSettings.vue b/src/views/settings/UserSettings.vue deleted file mode 100644 index 821268a..0000000 --- a/src/views/settings/UserSettings.vue +++ /dev/null @@ -1,44 +0,0 @@ - - -