Leaf integration system
Every "linked thing" on an Open Register object — a meeting, a contact, a chat room, a wiki page — is a leaf. Each leaf implements the same provider contract on the backend and registers the same way on the frontend. The result: one sidebar tab pattern, one widget pattern, one admin page that lists every leaf with its health status. Apps install only the leaves they need; the rest stay hidden.
This page documents the system. Each leaf has its own page under Integrations.
What a leaf gives you
- A sidebar tab on every object detail page. One tab per registered leaf, filtered to the apps the user has installed.
- A dashboard widget in four surfaces: user-dashboard, app-dashboard, detail-page, single-entity. The same registration drives all four.
- A reference-property renderer. A schema property typed as
referenceType: '<leaf-id>'renders the linked entity inline inCnFormDialogandCnDetailGrid. - An admin row under Administration → Open Register → Integrations. Reports install state, auth status, storage strategy, and a deep-link to configure the upstream source.
- An OCS capability entry under
openregister.integrations.providers. Clients discover the registry surface without probing routes.
All of this for one provider class and one registry descriptor per leaf.
The 18 leaves Open Register ships
filesFiles attached to an object. Always available.
notesFree-form notes on an object. Always available.
tagsSystem tags on an object. Always available.
tasksTo-do items on an object. Always available.
audit-trailEvery change to an object. Read-only, always available.
sharesNC Share Manager-backed share visibility per object.
calendarCalDAV meetings linked to an object.
contactsvCard contacts linked to an object, with role.
emailLink existing NC Mail messages to an object.
talkTalk conversations linked to an object. Provider stub.
bookmarksNC Bookmarks linked to an object. Provider stub.
collectivesCollectives pages linked to an object. Provider stub.
mapsNC Maps locations linked to an object. Provider stub.
photosNC Photos linked to an object with EXIF metadata. Provider stub.
activityNC Activity events relevant to an object. Read-only stub.
analyticsNC Analytics reports linked to an object. Provider stub.
cospendNC Cospend projects/bills linked to an object. Provider stub.
deckNC Deck cards linked to or created from an object.
flowNC Flow rules scoped to a schema/object. Provider stub.
formsNC Forms responses linked to an object. Provider stub.
pollsNC Polls linked to an object. Provider stub.
time-trackerNC time tracking entries linked to an object. Provider stub.
xwikiXWiki pages linked to an object. Routed through OpenConnector.
openprojectOpenProject work packages. Routed through OpenConnector.
How a leaf is wired
Each leaf has three pieces. The provider is server-side; the registration is in @conduction/nextcloud-vue; the activation is in the consuming app's main.js. Every consuming app picks up every leaf automatically — there is no per-app glue.
1. PHP provider (openregister/lib/Service/Integration/Providers/)
class CalendarProvider extends AbstractIntegrationProvider
{
public function getId(): string { return 'calendar'; }
public function getLabel(): string { return $this->l10n->t('Meetings'); }
public function getIcon(): string { return 'Calendar'; }
public function getGroup(): ?string { return 'comms'; }
public function getRequiredApp(): ?string { return 'calendar'; }
public function getStorageStrategy(): string { return 'link-table'; }
public function isEnabled(): bool { return $this->appManager->isInstalled('calendar'); }
public function list(string $register, string $schema, string $objectId, array $filters=[]): array
{
return $this->calendarEventService->getEventsForObject(objectUuid: $objectId);
}
// get / create / update / delete as the storage strategy supports.
}
The provider is registered with the DI container in Application::register() and pushed onto the IntegrationRegistry in Application::boot(). Storage strategy is 'magic-column' | 'link-table' | 'external' | 'query-time' (AD-22).
2. Vue registration (@conduction/nextcloud-vue/src/integrations/builtin/leaves.js)
import CnIntegrationTab from '../../components/CnIntegrationTab/CnIntegrationTab.vue'
import CnIntegrationCard from '../../components/CnIntegrationCard/CnIntegrationCard.vue'
window.OCA.OpenRegister.integrations.register({
id: 'calendar',
label: t('myapp', 'Meetings'),
icon: 'Calendar',
group: 'comms',
requiredApp: 'calendar',
order: 20,
referenceType: 'calendar',
tab: CnIntegrationTab,
widget: CnIntegrationCard,
defaultSize: { w: 4, h: 3 },
})
The generic CnIntegrationTab + CnIntegrationCard drive every leaf until any individual leaf needs a bespoke component, at which point the registration's tab / widget is repointed at a dedicated Vue file (the xWiki leaf has its own CnXwikiTab / CnXwikiCard already).
3. App-side activation ({consuming-app}/src/main.js)
import {
installIntegrationRegistry,
registerBuiltinIntegrations,
registerLeafIntegrations,
} from '@conduction/nextcloud-vue'
installIntegrationRegistry()
registerBuiltinIntegrations() // 5 always-on (files, notes, tags, tasks, audit-trail)
registerLeafIntegrations() // 18 NC-app and external leaves
That's the full wiring. Three calls. Every leaf the user has the required NC app for shows up automatically.
Storage strategies
Leaves declare one of four storage strategies. The registry uses this to choose the dispatch path; the docs page for each leaf explains the specifics.
| Strategy | Where the link lives | Example leaves |
|---|---|---|
magic-column | A column on the object's table row. Cheapest. | Files |
link-table | A dedicated join table (openregister_{leaf}_links). | Notes, Tags, Tasks, Calendar, Contacts, Deck, Email |
external | Nowhere local. Routed through OpenConnector on every CRUD. | xWiki, OpenProject |
query-time | Nowhere local. Computed fresh on every list() call. | Audit trail, Activity, Shares |
'query-time' providers throw NotImplementedException on create() / update() / delete() per AD-22 — there is no local store to write to.
Required-app gating
Every NC-app-backed leaf declares its requiredApp. The registry filters in three stages (AD-5):
- PHP
isEnabled()— returns false when the required NC app isn't installed. The OCS capabilities response marks the leafenabled: false. - JS-side filter —
CnObjectSidebar :use-registryhonours the same gate. Disabled leaves don't render a tab. - Admin UI — the integrations page still lists disabled leaves so admins know what's available, with a "needs
<app>installed" message and an install hint.
Built-in leaves (files, notes, tags, tasks, audit-trail) and the shares core leaf return requiredApp: null — they ride on Open Register itself and are always available.
Collision policy (AD-13)
Re-registering an existing leaf id is a no-op in production (the first registration wins) and throws in development. So a consuming app can pre-register a leaf id with a bespoke tab / widget to override the generic pair without touching the library:
import CnMyAppCalendarTab from './components/CnMyAppCalendarTab.vue'
import CnMyAppCalendarCard from './components/CnMyAppCalendarCard.vue'
// Run this BEFORE registerLeafIntegrations() so the override wins.
window.OCA.OpenRegister.integrations.register({
id: 'calendar', // same id as the generic registration
label: t('myapp', 'Meetings'),
icon: 'Calendar',
group: 'comms',
requiredApp: 'calendar',
tab: CnMyAppCalendarTab, // bespoke
widget: CnMyAppCalendarCard, // bespoke
})
registerLeafIntegrations() // no-op on 'calendar' — first wins
This is how xWiki ships its richer CnXwikiTab (breadcrumbs, text-preview) on top of the same id: 'xwiki' slot.
Surfaces (AD-19)
Every leaf renders in up to four surfaces from the same registration. The widget component receives a surface prop and branches on it.
| Surface | Where it appears | Default behaviour |
|---|---|---|
detail-page | Object detail page | Full linked-list with row actions |
user-dashboard | Personal Open Register dashboard | Compact list, max 5 entries |
app-dashboard | Per-app dashboard widget | Compact list, scoped to the app |
single-entity | A schema property of type reference with referenceType: '<id>' | A chip resolved by id |
The registration descriptor can pass surface-specific overrides (widgetCompact, widgetExpanded, widgetEntity); absent overrides fall back to the main widget.
What to read next
- xWiki leaf — the worked external example (OpenConnector-backed).
- Calendar leaf — the worked NC-native example (CalDAV link-table).
- Pluggable integration registry reference — the full ADR-019 contract.
- Verification report — live status for all 24 advertised providers from the smoke harness.
- Build your own leaf —
scripts/scaffold-integration.sh <id>in the openregister repo generates the openspec change, PHP provider stub, and JS registration stub.
Pre-register an id before registerLeafIntegrations() and you can ship a bespoke Vue tab / widget for any leaf. The library never clobbers an existing registration.