Skip to content
This repository was archived by the owner on May 29, 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
10 changes: 10 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -1736,6 +1736,16 @@ private function registerEventListeners(IRegistrationContext $context): void
MailAppScriptListener::class
);

// IntegrationGlobalScriptListener loads the shared integration-registry
// bootstrap on EVERY full-page render so the registry is installed +
// populated universally — letting leaves render inside any consuming
// app's object detail page (e.g. an OpenCatalogi publication) without
// that app bootstrapping the registry itself (universal-shared-integration-registry).
$context->registerEventListener(
\OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent::class,
\OCA\OpenRegister\Listener\IntegrationGlobalScriptListener::class
);

// CommentsEntityListener registers "openregister" objectType for Nextcloud Comments.
$context->registerEventListener(CommentsEntityEvent::class, CommentsEntityListener::class);

Expand Down
77 changes: 77 additions & 0 deletions lib/Listener/IntegrationGlobalScriptListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

/**
* Listener that injects the global integration-registry bootstrap script.
*
* SPDX-License-Identifier: EUPL-1.2
* SPDX-FileCopyrightText: 2026 Conduction B.V.
*
* @category Listener
* @package OCA\OpenRegister\Listener
*
* @author Conduction Development Team <dev@conduction.nl>
* @copyright 2026 Conduction B.V.
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* @version GIT: <git-id>
*
* @link https://www.OpenRegister.app
*
* @spec openspec/changes/universal-shared-integration-registry/tasks.md
*/

declare(strict_types=1);

namespace OCA\OpenRegister\Listener;

use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;

/**
* Loads the `openregister-integration-global` bundle on EVERY full-page
* render so the shared integration registry
* (`window.OCA.OpenRegister.integrations`) is installed + populated with
* the built-in integrations and generic leaves on every page — not just
* OpenRegister's own SPA.
*
* This is what lets integration tabs/widgets (and any leaf app's Path-2
* component) render inside ANY consuming app's object detail page (e.g.
* an OpenCatalogi publication) WITHOUT that app bootstrapping the
* registry itself. The bundle's `ensureIntegrationRegistry()` is
* idempotent, so co-loading it with OpenRegister's own main bundle is
* harmless.
*
* Unconditional by design: any page may host a CnDetailPage /
* CnObjectSidebar that reads the registry, so the bootstrap must be
* universally available. The script itself is tiny and short-circuits
* after the first run.
*
* @template-implements IEventListener<Event>
*
* @psalm-suppress UnusedClass
*
* @spec openspec/changes/universal-shared-integration-registry/tasks.md
*/
class IntegrationGlobalScriptListener implements IEventListener
{
/**
* Handle the template-rendered event by injecting the bootstrap script.
*
* @param Event $event The dispatched event.
*
* @return void
*
* @spec openspec/changes/universal-shared-integration-registry/tasks.md
*/
public function handle(Event $event): void
{
if (($event instanceof BeforeTemplateRenderedEvent) === false) {
return;
}

Util::addInitScript('openregister', 'openregister-integration-global');

}//end handle()
}//end class
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Universal Shared Integration Registry (global bootstrap)

## Problem

The pluggable integration registry (`pluggable-integration-registry`) installs
`window.OCA.OpenRegister.integrations` and renders integration tabs/widgets via
`CnObjectSidebar`, `CnDashboardPage`, and `CnDetailPage`. But the registry is
only installed + populated by **OpenRegister's own webpack bundles**
(`main`, `adminSettings`, `filesSidebar`, `mailSidebar`). On a page served by a
**consuming app** (e.g. an OpenCatalogi publication detail page), OpenRegister's
bundle never runs, so:

1. A leaf app (e.g. OpenConnector) that loads its Path-2 component bundle and
calls `registerIntegration(...)` only ever populates a **stub** registry
(`{_queue, register}`) — nothing drains it, because the drain happens inside
`installIntegrationRegistry`, which only OpenRegister calls.
2. `useIntegrationRegistry()` in a foreign app's bundle reads its **own
per-bundle module singleton**, never the window-global the leaf queued onto.

Net effect: the leaf's "Synced from" tab/widget never renders outside
OpenRegister's own SPA, even though the descriptor was queued. The whole point
of the leaf system — extend OpenRegister **without changing its tables or code**
and have leaves surface **inside any consuming app** — is unmet.

## Solution

Ship a tiny global bootstrap bundle and load it on **every** full-page render:

- **`src/integration-global.js`** — new webpack entry
(`openregister-integration-global.js`) that imports and calls the existing
`ensureIntegrationRegistry()`. Idempotent + tiny.
- **`src/integrations/bootstrap.js`** — `ensureIntegrationRegistry()` now
resolves the **shared** registry via `getSharedRegistry(window)`
(nc-vue `universal-shared-integration-registry`: converge-not-clobber +
install-if-needed) and registers builtins + leaves into *that* instance, so
every consuming app's `useIntegrationRegistry()` — which now defaults to the
same shared window-global via `sharedRegistryIfInstalled()` — sees them.
- **`lib/Listener/IntegrationGlobalScriptListener.php`** — listens on
`BeforeTemplateRenderedEvent` and calls
`Util::addInitScript('openregister', 'openregister-integration-global')`
unconditionally, so the registry is installed + populated on every page,
not just OpenRegister's.

This requires **zero changes** to any consuming app: an OpenCatalogi
publication page now hosts a fully-populated shared registry, and any leaf
(OpenConnector's `sync-contract`) that queued a descriptor renders its tab/widget.

## Out of scope

- The nc-vue reconciliation primitives (`getSharedRegistry`,
`sharedRegistryIfInstalled`, converge-not-clobber `installIntegrationRegistry`,
composable shared-default) — landed in nc-vue beta (ncv#443).
- Externalizing Vue/nc-vue from leaf integration bundles (follow-up).
20 changes: 20 additions & 0 deletions openspec/changes/universal-shared-integration-registry/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Tasks: universal shared integration registry (OpenRegister global bootstrap)

- [x] `src/integration-global.js` — new webpack entry that calls
`ensureIntegrationRegistry()`.
- [x] `webpack.config.js` — add `integrationGlobal` entry →
`openregister-integration-global.js`.
- [x] `src/integrations/bootstrap.js` — `ensureIntegrationRegistry()` resolves
the shared registry via `getSharedRegistry(window)` and registers
builtins + leaves into it (was `installIntegrationRegistry`).
- [x] `lib/Listener/IntegrationGlobalScriptListener.php` — load the global
bundle on every `BeforeTemplateRenderedEvent`.
- [x] `lib/AppInfo/Application.php` — register the listener.
- [x] Build green; `openregister-integration-global.js` produced + deployed.

## Verification

- [ ] On an OpenCatalogi publication detail page (ZERO OpenCatalogi changes):
`window.OCA.OpenRegister.integrations` is a real registry (not a stub),
contains the built-ins + OpenConnector's `sync-contract` leaf, and the
"Synced from" tab/widget renders.
24 changes: 24 additions & 0 deletions src/integration-global.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Global integration-registry bootstrap entry.
*
* Loaded on EVERY Nextcloud page via `OCP\Util::addInitScript` (see
* lib/AppInfo/Application.php) so the shared registry
* (window.OCA.OpenRegister.integrations) is installed + populated with the
* built-in integrations and the generic leaves on every page — not just
* OpenRegister's own SPA. That makes integration tabs/widgets (and any
* leaf app's Path-2 component queued on the stub) render inside ANY
* consuming app's object detail page (e.g. an OpenCatalogi publication)
* with zero per-consumer bootstrap.
*
* Kept tiny + idempotent (ensureIntegrationRegistry guards re-entry), so
* loading it alongside OpenRegister's own main bundle is harmless.
*
* @package OpenRegister
*
* @license EUPL-1.2
*
* @see ADR-019 — Pluggable Integration Registry
*/
import { ensureIntegrationRegistry } from './integrations/bootstrap.js'

ensureIntegrationRegistry()
14 changes: 10 additions & 4 deletions src/integrations/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* @see ADR-019 — Pluggable Integration Registry
*/
import {
installIntegrationRegistry,
getSharedRegistry,
registerBuiltinIntegrations,
registerLeafIntegrations,
} from '@conduction/nextcloud-vue'
Expand All @@ -25,13 +25,19 @@ let bootstrapped = false
/**
* Idempotent — safe to call from every entry bundle. Subsequent calls
* after the first are no-ops, so consumers don't need to coordinate.
*
* Resolves the SHARED registry (window-global) via getSharedRegistry and
* registers builtins + leaves into THAT instance, so every consuming
* app's useIntegrationRegistry (which reads the same shared instance)
* sees them — including when this bootstrap runs from the global
* init-script on a foreign app's page (e.g. an OpenCatalogi publication).
*/
export function ensureIntegrationRegistry() {
if (bootstrapped) {
return
}
installIntegrationRegistry(window)
registerBuiltinIntegrations()
registerLeafIntegrations()
const registry = getSharedRegistry(window)
registerBuiltinIntegrations(registry)
registerLeafIntegrations(registry)
bootstrapped = true
}
9 changes: 9 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ webpackConfig.entry = {
import: path.join(__dirname, 'src', 'main.js'),
filename: appId + '-main.js',
},
// Global registry bootstrap (universal-shared-integration-registry).
// Loaded on EVERY page via \OCP\Util::addInitScript so the shared
// integration registry is installed + populated everywhere, letting
// leaves render inside any consuming app's detail page without that
// app bootstrapping the registry itself. Kept separate + tiny.
integrationGlobal: {
import: path.join(__dirname, 'src', 'integration-global.js'),
filename: appId + '-integration-global.js',
},
adminSettings: {
import: path.join(__dirname, 'src', 'settings.js'),
filename: appId + '-settings.js',
Expand Down
Loading