feat: Unified Nextcloud entity linking system#1237
feat: Unified Nextcloud entity linking system#1237rubenvdlinde wants to merge 151 commits intodevelopmentfrom
Conversation
Proposals for centralized integration between OpenRegister and Nextcloud surfaces (Files, Mail, Contacts, Calendar, Activity, Workflow, Profile). Changes: - action-registry: Core event-based action registration with APCu caching - files-sidebar-tabs: Entities/Objects/Actions tabs in Files sidebar - file-actions: Right-click context menu actions from consuming apps - mail-sidebar: Entities/Objects/Actions in Mail message detail - mail-smart-picker: Smart Picker reference provider for Mail/Text/Talk - contacts-actions: ContactsMenu integration with entity matching - activity-provider: Centralized activity stream for all consuming apps - calendar-provider: Object dates as calendar events - workflow-operations: Nextcloud Flow automation operations - profile-actions: User profile link actions with entity mapping Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…hanges Design docs, detailed specs (GIVEN/WHEN/THEN), and task breakdowns for: - action-registry: 444-line spec, 46 scenarios, 14 tasks - files-sidebar-tabs: 251-line spec, 30+ scenarios, 11 tasks - file-actions: 435-line spec, 50+ scenarios, 88 tasks - mail-sidebar: 442-line spec, 30+ scenarios, 55 tasks - mail-smart-picker: 236-line spec, 24 scenarios, 22 tasks - contacts-actions: 261-line spec, 25+ scenarios, 30 tasks - activity-provider: 265-line spec, 26 scenarios, 42 tasks - calendar-provider: 382-line spec, 35+ scenarios, 15 task groups - workflow-operations: 267-line spec, 33 scenarios, 23 tasks - profile-actions: 343-line spec, 38 scenarios, 29 tasks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Search result subtitles previously showed raw numeric IDs like "Schema: 498 • Register: 228". Now resolves to human-readable titles like "Client • Pipelinq" using SchemaMapper and RegisterMapper with a per-request name cache to avoid repeated lookups. Closes #975
- tmlo-metadata: TMLO metadata standard support for archival compliance - nextcloud-entity-relations: Nextcloud entity relations integration
Create the openregister_email_links table with EmailLink entity and EmailLinkMapper for linking Nextcloud Mail messages to OpenRegister objects. Includes indexes on message, sender, and object UUID. Closes #1007
Implements findByMessageId, findObjectsBySender, quickLink, and deleteLink methods with register/schema title resolution. Closes #1008
Implements byMessage, bySender, quickLink, and deleteLink endpoints with input validation and proper HTTP status codes. Closes #1009
Adds routes for byMessage, bySender, quickLink, and deleteLink endpoints to support the Mail app sidebar integration. Closes #1010
Creates listener that injects the mail sidebar script when the Mail app renders, with checks for Mail app enabled status and user register access. Closes #1011
Adds separate webpack entry point for the mail sidebar bundle that mounts a Vue component into the Mail app DOM. Closes #1012
Creates mail-sidebar.css using Nextcloud CSS variables for dark theme support and WCAG AA accessible focus indicators. Responsive layout with 320px panel on desktop, overlay on narrow viewports. Closes #1015
…, MailAppScriptListener EmailServiceTest: 7 tests covering findByMessageId, findObjectsBySender, quickLink (success + duplicate), deleteLink, and empty results. EmailsControllerTest: 11 tests covering byMessage, bySender, quickLink, deleteLink with all validation and error paths. MailAppScriptListenerTest: 4 tests covering non-mail events, no user, mail app disabled, and no register access scenarios. Closes #1016
Archives mail-sidebar change artifacts, syncs spec to main specs directory with status=implemented, and creates feature documentation.
…u cache Implements shared service for matching Nextcloud contact metadata (email, name, organization) to OpenRegister entities. Features: - matchByEmail() with confidence 1.0 and APCu caching (TTL 60s) - matchByName() with 0.7 full / 0.4 partial confidence scoring - matchByOrganization() filtered to org-type schemas, confidence 0.5 - matchContact() combining all three with UUID-based deduplication - getRelatedObjectCounts() grouping matches by schema title - Cache invalidation per email and per object Closes #1023, closes #1024
Implements OCP\Contacts\ContactsMenu\IProvider that bridges Nextcloud Contacts with OpenRegister entities. Features: - Extracts email/name/org from contact entries - Matches against OpenRegister entities via ContactMatchingService - Injects count badge (e.g., "3 Zaken, 1 Lead") with highest priority - Adds per-entity "View in OpenRegister" actions with deep link support - Graceful degradation when action registry unavailable - All exceptions caught and logged at warning level Closes #1025
Create migration Version1Date20260325120000 with three new tables: - openregister_email_links (Mail message to object links) - openregister_contact_links (CardDAV contact to object links) - openregister_deck_links (Deck card to object links) Add Entity and Mapper classes for each link type with findByObjectUuid(), deleteByObjectUuid(), and type-specific query methods. Closes #1071
- Register contacts menu provider in appinfo/info.xml via <contactsmenu> element so Nextcloud auto-discovers the provider - Add cache invalidation in ObjectService::saveObject() that calls ContactMatchingService::invalidateCacheForObject() after persistence to bust stale APCu cache entries for email-matched contacts Closes #1026
Implements API endpoint for matching contact metadata against OpenRegister
entities, reusable by mail-sidebar and other consumers. Features:
- GET /api/contacts/match?email={email}&name={name}&organization={org}
- Validates at least email or name is provided (400 otherwise)
- Returns matches enriched with url/icon from deep link registry
- Includes total count and cache hit indicator
- Route added before wildcard routes in routes.php
Closes #1027
…ruction-workflow Creates migration Version1Date20260325120000 with two new tables: - openregister_selection_lists: stores retention rules per category - openregister_destruction_lists: tracks destruction workflow lists Also creates openspec change artifacts (proposal, design, specs, tasks, plan). Closes #1113
Adds the <activity> section declaring Provider, ObjectSetting, RegisterSetting, SchemaSetting, and Filter for Nextcloud auto-discovery. Closes #1107
EmailService manages email-to-object links via openregister_email_links table. Features: link/unlink emails, search by sender, duplicate detection (409), Mail app availability check (501), cached metadata from Mail DB. EmailsController exposes REST endpoints: GET/POST/DELETE /emails, GET /emails/search. Closes #1079
Add optional contactContext parameter to resolveUrl() for contact-specific
placeholder resolution:
- {contactId} - URL-encoded contact vCard UID
- {contactEmail} - URL-encoded contact email
- {contactName} - URL-encoded contact display name
- {entityId} - alias for the matched object UUID
Contact placeholders are resolved after object-level placeholders, allowing
both to coexist in the same URL template. Fully backward compatible.
Closes #1028
SelectionList maps classification categories to retention periods and archival actions (vernietigen/bewaren) per Dutch selectielijst standards. Mapper includes findByCategory(), findByUuid(), and findAll() methods. Closes #1114
…extcloud-entity-relations # Conflicts: # package.json
- Auto-fix 1207 PHPCS violations via PHPCBF (formatting, spacing, etc.) - Add named parameters to all internal method calls (custom sniff) - Add missing short descriptions to @var and @method docblocks - Fix implicit true comparisons (use explicit === true/false) - Fix line length violations by extracting helpers and breaking strings - Fix ESLint: reserved component name Circle→CircleIcon, unused params - Add @nextcloud/vue-richtext dependency for reference widget - Add .gitignore entry for docs/node_modules/
Add a generic system for linking Nextcloud entities (mail, contacts, calendar, notes, todos, talk, deck) to OpenRegister objects and entities. Two mechanisms: - Schema configuration.linkedTypes: declarative array controlling which sidebar integrations OpenRegister injects into other apps - Nc* property types (NcMail, NcContact, etc.): typed schema properties storing reference envelopes with id/type/label Implementation: - Lean _mail, _contacts, _notes, _todos, _calendar, _talk, _deck metadata columns on magic tables (based on linkedTypes) and entity tables - SaveObject pipeline LinkedEntityPropertyHandler extracts Nc* property IDs into metadata columns, preserving ad-hoc sidebar links - LinkedEntityEnricher for read-time _extend hydration from NC native APIs - Generic LinkedEntityController with add/remove/reverse-lookup endpoints - MailAppScriptListener checks linkedTypes before sidebar injection - Frontend sidebar migrated to generic API endpoints - Migrations to add columns to entity tables and drop old link tables
The enricher was implemented but not called from the response pipeline. Now _extend[_mail], _extend[_contacts] etc. trigger enrichment of linked entity IDs into full objects from Nextcloud native APIs.
- Create new main spec: openspec/specs/linked-entity-types/spec.md with all 9 requirements (linkedTypes, Nc* types, metadata columns, pipeline, enrichment, API, reverse lookup, sidebar, email removal) - Update object-interactions spec: notes now sync to _notes metadata column
…y-relations Merges the comprehensive spec branch which includes Activity provider, Calendar provider, Contacts menu, File sidebar, Actions/Workflow, Archival/Destruction, TMLO metadata, Approval chains, Scheduled workflows, Profile actions, and Smart picker — alongside our linked-entity-types work. Conflicts resolved by keeping our refactored MailAppScriptListener (uses SchemaMapper + linkedTypes) and package.json, while taking their newer quality fixes for controllers, services, and other non-overlapping code.
…metadata columns Services now use _mail, _contacts, _deck metadata columns on ObjectEntity instead of dedicated link tables (email_links, contact_links, deck_links). Changes per service: - EmailService: MagicMapper + LinkedEntityService, removed EmailLinkMapper - ContactService: MagicMapper + LinkedEntityService, removed ContactLinkMapper (keeps CardDAV vCard X-OPENREGISTER-* sync) - DeckCardService: MagicMapper + LinkedEntityService, removed DeckLinkMapper (keeps Deck card creation via CardService) Breaking API changes: - unlinkEmail(linkId) → unlinkEmail(objectUuid, mailRef) - unlinkContact(linkId) → unlinkContact(objectUuid, contactUid) - unlinkCard(linkId) → unlinkCard(objectUuid, deckRef) - Route params updated from numeric IDs to string entity references Removed: EmailLink, EmailLinkMapper, ContactLink, ContactLinkMapper, DeckLink, DeckLinkMapper (6 files)
Synced refactor-link-services delta specs into main linked-entity-types spec: - Added 4 requirements: Specialized Services Use Metadata Columns, Reverse Lookups Via LinkedEntityService, Remove Link Entities/Mappers, Controller API Signature Changes Archived to: - archive/2026-03-26-linked-entity-types/ - archive/2026-03-26-refactor-link-services/
- EmailsController: fix unlinkEmail call to pass (uuid, mailRef), fix linkEmail return (now array, not entity), remove dead methods (quickLink, deleteLink, byMessage — routes already removed) - ContactsController: fix unlinkContact to pass (uuid, contactUid), fix linkContact return, update route param to contactUid - DeckController: fix unlinkCard to pass (uuid, deckRef), fix linkOrCreateCard return
The listener was never registered in Application.php (missing from merged branches). Now registered for BeforeTemplateRenderedEvent from core. Also fixed the listener to: - Use core BeforeTemplateRenderedEvent (not Mail-specific event) - Check request URI for /apps/mail to only inject on Mail pages - Inject IRequest dependency for URL detection
- Route requirements for entityId changed from [^/]+ to .+ to support mail IDs like "1/5" which contain slashes - Confirmed MailAppScriptListener works: sidebar appears when schema has linkedTypes: ["mail"], hidden otherwise
…arch quoting - ObjectEntity.getObjectArray() now includes _mail, _contacts, _notes, _todos, _calendar, _talk, _deck in @self metadata - MagicSearchHandler: quote column names in search ILIKE conditions to handle PostgreSQL reserved words (e.g., 'case', 'status')
Three critical fixes for the linked entity system: 1. MagicMapper.prepareObjectDataForTable(): dynamically includes linked type fields (mail, contacts, etc.) in the metadata fields list based on schema.getLinkedTypes(). Only active types are included to avoid writing to non-existent columns. 2. ObjectEntity.getObjectArray(): includes linked type fields (mail, contacts, notes, todos, calendar, talk, deck) in @self metadata so they appear in API responses and are available for the prepare method. 3. MagicSearchHandler.buildSearchConditionSql(): quotes column names with double-quotes (PostgreSQL) or backticks (MySQL) to handle reserved words like 'case' and 'status' in schema property names. Verified end-to-end: add link via API → persisted to PostgreSQL → reverse lookup finds object → object GET returns _mail in @self.
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (147 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
- Added mailSidebar entry to webpack.config.js (was missing from merge) - Rewrote useMailObserver to support path-based routing (/apps/mail/box/ priority/thread/6) in addition to legacy hash routing - Added URL polling (500ms) to detect Vue Router pushState navigation - Added debug console logging to mail-sidebar.js entry point - Rebuilt JS with npm run build
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (147 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
The Mail app's Vue instance owns #app-content-vue and destroys injected children during re-renders. Now mounts the sidebar container as a sibling inside #content-vue, which is outside Vue's reconciliation scope.
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (147 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Helps debug silent mount failures. Vue instantiation errors are now caught and logged to console instead of being silently swallowed.
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (147 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
The Mail app's Vue instance destroys DOM children during re-renders, causing any sidebar injected inside Vue-managed containers to disappear. Fix: use Vue.$mount() without el, then append $el to document.body. This places the sidebar completely outside any Vue reconciliation scope. Also simplified mount detection to check for .or-mail-sidebar class instead of container ID, since we no longer create a wrapper div.
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (147 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Replace the flat sidebar layout with a tabbed interface: Actions tab: - Shows one "Link to [SchemaName]" block per schema with linkedTypes: ["mail"] - Searchable input field queries /api/objects with _search= for live filtering - RBAC is auto-applied by the objects endpoint - Clicking a result links the object to the current email via the generic API Objects tab: - Shows objects already linked to the current email (reverse lookup) - Unlink button per object Entities tab: - Shows NLP-detected entities (persons, organizations, emails, etc.) - Queries the /api/entities endpoint for the current email - Groups entities by type with confidence scores Removed the generic "Link to Object" dialog — replaced by the schema-specific searchable selects in the Actions tab.
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (147 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Replaces custom tab CSS with Nextcloud's native sidebar tab components, giving the exact same look as the NcAppSidebar in Procest case detail. Tabs: Actions (icon-add), Entities (icon-user), Objects (icon-link). NcAppSidebar handles header, tabs, tab content, and close button. Custom scoped CSS overrides position to work inside our fixed container.
Quality Report
Summary
PHP Quality
Vue Quality
Security
License Compliance
composer dependencies (147 total)
PHPUnit TestsPHPUnit tests were not enabled for this run. Integration Tests (Newman)Newman integration tests were not enabled for this run. E2E Tests (Playwright)Playwright E2E tests were not enabled for this run. Generated automatically by the Quality workflow.
|
Summary
configuration.linkedTypescontrols which entity types are enabled per schemaNcMail,NcContact, etc.) for typed field-level references with standardized envelope format_mail,_contacts,_decketc. metadata columns on magic tables (based on linkedTypes) and entity tablesLinkedEntityPropertyHandlerin SaveObject pipeline extracts Nc* property IDs into metadata columnsLinkedEntityEnricherfor read-time hydration via_extend[_mail]etc.LinkedEntityControllerwith generic add/remove/reverse-lookup endpointsMailAppScriptListenerchecks linkedTypes before injecting mail sidebarEmailService,ContactService,DeckCardServiceto use metadata columns instead of dedicated link tablesemail_links,contact_links,deck_linkstablesAPI Endpoints
Generic Linked Entity API
/api/objects/{uuid}/_linked/{type}/api/objects/{uuid}/_linked/{type}/{entityId}/api/registers/{uuid}/_linked/{type}/api/schemas/{uuid}/_linked/{type}/api/linked/{type}/{entityId}Breaking Changes
unlinkEmail(linkId)→unlinkEmail(objectUuid, mailRef)unlinkContact(linkId)→unlinkContact(objectUuid, contactUid)unlinkCard(linkId)→unlinkCard(objectUuid, deckRef)Test Plan