You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Note: This design proposal was drafted with AI assistance (Claude Code, model claude-opus-4-8) and is posted for discussion. Details are subject to review and change.
Proposed design: a framework-agnostic event editor plugin API
A concrete proposal for this ticket. The goal: let any Nextcloud app add custom inputs to the event editor and read/modify the event being edited — both the standard editor properties and arbitrary custom ICS properties — without depending on Calendar's internals or its frontend framework.
Scope (this iteration)
✅ Custom editor fields (UI injected into the event editor)
✅ First-class access to the editor's working model (standard properties)
✅ An ICS serialization hook (influence the generated iCalendar data)
❌ Out of scope for now: event context-menu actions, calendar-grid/view plugins, settings hooks. These can reuse the same registry later (see APIs for other apps #812).
Design decisions
Framework-agnostic UI — plugin UI is a Web Component (custom element). Calendar only renders <your-tag> and assigns it a context object. The plugin can be written in vanilla JS, Lit, Svelte, React, Vue — anything that compiles to a custom element. No coupling to Calendar's Vue version. (Same mechanism the Files app uses for sidebar tabs.)
Framework-agnostic registration — a tiny published package whose functions only write to a window global. Plugins never import Calendar code.
The editor model is the primary surface — plugins operate on a stable, reactive projection of the event (the same data the built-in fields edit), not the Pinia store and not (primarily) raw ICS strings.
No restriction on property names — plugins may read/write any ICS property; docs recommend an X-<APPID>-… prefix but it is not enforced.
Load-order-proof — a drained queue (Viewer pattern) + a live registry (Files pattern), so registration works whether the plugin script runs before or after Calendar boots.
Public API
Working package name (TBD — see open questions): @nextcloud/calendar-plugin. Zero runtime deps; ships types + thin functions writing to window globals. EventComponent re-exported type-only from @nextcloud/calendar-js.
Register a custom field
import{registerEventEditorField}from'@nextcloud/calendar-plugin'interfaceEventEditorField{id: string// unique; prefix with your app idtagName: string// custom element tag (needs a hyphen); prefix with app idorder?: number// sort among plugin fieldsvariants?: Array<'full'|'simple'>// default ['full']enabled?: (ctx: EventContext)=>boolean// re-evaluated on changeonInit?: ()=>void|Promise<void>// define the custom element once (lazy import)}registerEventEditorField(field: EventEditorField): void
Calendar renders the element and assigns the context as a property (el.context = ctx) — which is what lets us hand over a rich object with callbacks rather than just string attributes.
Register a UI-less ICS hook
interfaceEventSerializeHook{id: string// called once per VEVENT, synchronously, immediately before toICS()serialize: (component: EventComponent,ctx: EventContext,scope: SaveScope)=>void}registerEventSerializeHook(hook: EventSerializeHook): void
The context — centered on the live event model
typeSaveScope='this'|'thisAndFuture'|'all'interfaceEventContext{/** * Live view of the editor's working model — the SAME data the built-in editor edits. * Reads return the current value. Writes route through the same store actions the * built-in fields use, so the default UI updates reactively (and vice versa). */readonlyevent: EventModel/** Subscribe to editor lifecycle. Returns an unsubscribe fn; auto-cleaned on close. */on(type: 'change'|'beforeSave'|'close',cb: ()=>void): ()=>void/** Escape hatch for custom/advanced ICS (X-*, parameters, multi-value, sub-components). */getProperty(name: string): string|nullgetPropertyValues(name: string): string[]setProperty(name: string,value: string|null): void// null deletes; marks dirtygetComponent(): EventComponent// raw calendar-js component}interfaceEventModel{// identity / recurrence (read-only)readonlyuid: stringreadonlyisNew: booleanreadonlyrecurring: booleanreadonlyinstanceType: 'master'|'occurrence'|'exception'// standard scalar fields — READ & WRITE (write-through to the built-in UI)summary: stringlocation: stringdescription: stringstatus: 'CONFIRMED'|'TENTATIVE'|'CANCELLED'|nullaccessClass: 'PUBLIC'|'PRIVATE'|'CONFIDENTIAL'|nulltimeTransparency: 'OPAQUE'|'TRANSPARENT'|nullcolor: string|null// time — READ & WRITEallDay: booleanstart: Dateend: DatestartTimezone: stringendTimezone: string// collections — READ always; WRITE via mutators (v1: categories + attendees)readonlycategories: readonlystring[]addCategory(value: string): voidremoveCategory(value: string): voidreadonlyattendees: readonlyAttendeeView[]addAttendee(a: {email: string,commonName?: string,role?: string,rsvp?: boolean}): voidremoveAttendee(email: string): void// read-only in v1 (use getComponent() for advanced writes)readonlyorganizer: {email: string,commonName: string}|nullreadonlyalarms: readonlyAlarmView[]readonlyattachments: readonlyAttachmentView[]readonlyrecurrenceRule: RecurrenceView|null}
event accessors map to the reactive calendarObjectInstance store state (read) and store actions (write) — the same path the built-in fields use, so everything stays consistent and reactive. getComponent()/get/setProperty map to the raw calendar-js eventComponent for anything the model doesn't cover.
Recurrence semantics
The model is O(1) per open editor — it does not grow with occurrence count. Opening an instance resolves a single component via getObjectAtRecurrenceId() → recurrenceManager.getOccurrenceAtExactly() (src/utils/calendarObject.js:34), projected into the single reactive calendarObjectInstance. A weekly-forever series is stored as master VEVENT + a handful of RECURRENCE-ID exceptions; other occurrences are computed, never stored. So there is no per-occurrence plugin state.
The event model and setProperty are scoped to the instance being edited, exactly like the built-in fields. For the common case ("add a field to this event"), a plugin needs zero recurrence awareness — the existing this-only / this-and-future save flow applies automatically (saveCalendarObjectInstance → eventComponent.createRecurrenceException(thisAndAllFuture), src/store/calendarObjectInstance.js:1519).
Reads see inherited values: an un-exceptioned occurrence is forked from the master (primaryItem), so getProperty() returns the master's value.
The raw accessor (getComponent()) is the escape hatch for genuinely cross-instance / series-level work: target the master while editing one occurrence (getComponent().primaryItem), enumerate the whole series/all exceptions (parent calendarComponent), or touch RRULE/RECURRENCE-ID structure (not in the v1 model).
For recurrence-aware plugins without dropping to raw, the context exposes event.instanceType / event.recurring and passes the chosen save scope into beforeSave/serialize, so a plugin can decide series-vs-occurrence placement (and observe that this-and-future creates a brand-new master with a new UID).
Runtime architecture (Calendar side)
Registration: the register* functions write to window._nc_calendar_editor_fields / _nc_calendar_serialize_hooks Maps and notify window.OCA.Calendar if it is already up. On boot, Calendar creates window.OCA.Calendar (façade + version + an EventTarget), drains the queues, and dispatches register:* for late registrations so an open editor updates live.
Editor zone: add one extension zone to EditFull.vue (bottom of the right column) and a compact one in EditSimple.vue (above SaveButtons, 'simple' variants only). A small host-internal wrapper runs onInit(), creates each enabled element, assigns el.context, and watches the calendarObjectInstance store to fire the context's change — the reactivity bridge to the framework-agnostic element.
Save/serialize pass: in saveCalendarObjectInstance(), before updateCalendarObject(): emit beforeSave, apply staged setProperty writes, run all serialize hooks on the (post-fork) eventComponent, then markDirty() if anything changed (so the isDirty() gate does not skip the save). updateCalendarObject() then does the single toICS() + PUT (src/store/calendarObjects.js:107).
Loading & timing (PHP)
Add an init-script entry point in rspack.config.js (Viewer-style main + init) so window.OCA.Calendar exists early.
ViewController dispatches a new OCA\Calendar\Event\LoadAdditionalScriptsEvent before render (mirrors the Files app). Plugin apps subscribe and call \OCP\Util::addInitScript('myapp', '…') → runs before the editor mounts, and only on calendar pages.
Correctness considerations
Custom-property preservation: the recurrence fork on save (createRecurrenceException) splits at the component level and preserves unknown/X-* properties (worth a regression test). However, the Duplicate event action goes through copyCalendarObjectInstanceIntoEventComponent() (src/models/event.js:192, called at calendarObjectInstance.js:1563), which copies only a whitelist and would drop custom properties. Fix: carry over all otherwise-unhandled properties there.
Two writers to standard fields: since plugins can write summary, location, etc., a plugin and the user can touch the same field. Routing through store actions keeps state consistent (last-writer-wins, UI updates live). Consider a dev-mode warning.
No namespace restriction (by design):setProperty/getComponent can overwrite RFC/core properties. Not enforced; docs recommend an X-<APPID>-… prefix.
Sync serialize only (v1): fetch remote data while the editor is open; write synchronously on beforeSave.
Trust: the element runs first-party in Calendar's origin (same trust level as a Files sidebar tab); the plugin owns sanitization of values it writes to ICS that other clients/invitees will parse.
Out of scope v1: public booking / embedded views; alarm/attachment/recurrence writes (read-only, with the raw-component escape hatch available).
Worked example
A plugin that reads standard fields (attendees) and writes a derived custom property, re-rendering when the user edits the rest of the event:
// registration (loaded via OCP\Util::addInitScript on calendar pages)import{registerEventEditorField}from'@nextcloud/calendar-plugin'registerEventEditorField({id: 'myapp-approval',tagName: 'myapp-approval-field',order: 10,variants: ['full','simple'],asynconInit(){const{ MyApprovalField }=awaitimport('./MyApprovalField.ts')if(!customElements.get('myapp-approval-field')){customElements.define('myapp-approval-field',MyApprovalField)}},})
// the custom element — no frameworkexportclassMyApprovalFieldextendsHTMLElement{privatectx!: import('@nextcloud/calendar-plugin').EventContextconnectedCallback(){constrender=()=>{constneedsApproval=this.ctx.event.attendees.length>10this.innerHTML=`<label><input type="checkbox" ${needsApproval ? 'checked' : ''}> Requires manager approval (${this.ctx.event.attendees.length} guests)</label>`this.querySelector('input')!.addEventListener('change',(e)=>{conston=(e.targetasHTMLInputElement).checkedthis.ctx.setProperty('X-MYAPP-NEEDS-APPROVAL',on ? 'TRUE' : null)})}render()this.ctx.on('change',render)// re-render when the user edits other fields}}
Prior art
Viewer (@nextcloud/viewer): framework-agnostic registration via a tiny npm package that writes handlers to a window._oca_viewer_handlers Map queue, drained by the app on init (loaded via addInitScript).
Files (@nextcloud/files): sidebar tabs declare a tagName and register a Web Component (customElements.define) — the framework-agnostic content pattern — plus a reactive getFilesRegistry() EventTarget for post-load registration.
This proposal combines both: Viewer's queue + npm registration, Files' Web-Component content + reactive registry, plus a calendar-js serialize hook specific to the "influence ICS generation" requirement.
Open questions
Distribution: in-repo api_package/ (Viewer-style, versioned with the app) first, vs. a dedicated @nextcloud/calendar-plugin repo now.
Final package + global names (@nextcloud/calendar-plugin, window.OCA.Calendar, the queue globals).
variants default — ['full'] only, or also 'simple' (popover is space-constrained)?
Dev-mode warning when a plugin overwrites a standard/RFC property — yes/no?
Scope of write access to standard fields in v1 (proposed: scalars + categories + attendees; alarms/attachments/recurrence read-only).
Rollout (focused, independently reviewable PRs)
Custom-property preservation fix in the Duplicate path (copyCalendarObjectInstanceIntoEventComponent) + unit tests.
Framework-agnostic UI — plugin UI is a Web Component (custom element). Calendar only renders and assigns it a context object. The plugin can be written in vanilla JS, Lit, Svelte, React, Vue — anything that compiles to a custom element. No coupling to Calendar's Vue version. (Same mechanism the Files app uses for sidebar tabs.)
In the past I encountered, issues with web components bundling a custom Vue version.
It is something that Vue does not intend to support.
See vuejs/core#14067
But there surly exist solutions from this from practitioners of Micro Frontends.
So, adding a open question: Do we want to take care of isolation between plugins and the calendar app (and between plugins and plugins)? Or do we leave the responsibility to plugin authors.
So, adding a open question: Do we want to take care of isolation between plugins and the calendar app (and between plugins and plugins)? Or do we leave the responsibility to plugin authors.
Currently multiple apps can already do load different versions of a library. For now we do plan to add further isolation.
A broken calendar extension should not break the calendar app
Log calendar extension name in related error messages
The data model (the one we might want to expose in the context) is not reactive yet
Migration to TypeScript is outstanding
From the last two points I conclude, that we should not directly expose our model in the this.ctx.
So currently I strongly leaning on having a dedicated type for the data/methods we expose to the extension and fully encapsulate and keep our existing data structures internal.
The extension API can the also be independently versioned.
Using a context property for custom components seems counterintuitive. I was directly thinking of plain element attributes (which can be reactively handled in custom components) and using events to communicate changes.
On the other hand, having the same context object/API for custom elements and hooks seems also good.
So I'm not sure about this one yet.
I will take a look at currently existing solutions in Nextcloud (e.g., extension in Viewer and Files) and other web projects.
I got some use cases from a potential extension author:
Hide existing inputs in the editor
For example, when the extension already provides its own custom element to edit that property
Some way to provide custom error messages
e.g., "event could not be saved because service XY is not available"
Some way to validate property values
validate and indicate that it is not a valid value
e.g., "event could not be saved because Y is not a valid option for Z"
Some way to disable the input of specific values
e.g., limit who can be a host to a meeting
(1) and (2) are not yet covered by the proposed extension points and would become custom extension points or some options, that can be dynamically set on this.ctx.
(2) and (4) can be covered by the custom elements because the extension author can display the validation result in custom components. But in the future, it might be interesting to have fine-grained hooks for our built-in components. Example for our attendee select field: Add custom logic to filter what attendees are allowed in a meeting and don't offer them to the user or highlight them as incompatible after the user selected them.
There are also global variables that acted as APIs in the past. The use of these variables is discouraged, as they lead to script loading order problems and the dependency hell, making it hard for the server component to update libraries.
So for consistency we would build the npm package @nextcloud/calendar around window._nc_calendar_scope.v1_0 for consistency.
(And during initial phase around window._nc_calendar_scope.v0_0
Probably there are a few more interesting implementation details in @nextcloud/files worth locking closer into.
Another use case: Limit what type of recurrences is allowed. This is use full if events are synced with other systems that support less recurrence rules then CalDav standard/our editor.
Some example:
Allow monthly recurring events to only occur once per month.
Allow yearly recurring events to occur only once per month.
Disallow overlapping recurrent events that overlap over the series
Implementing this with the current proposal would be do hide the repeat selector and add custom component to handle it.
If this becomes widely needed it might we could hide/restrict features behind capability checks that Nextcloud apps can toggle using the events editor API.
Another use case from the same corner, if it helps shape the scope: rendering the editor outside the Calendar app.
I have been prototyping a week view inside Talk that shows the events tied to your conversations. Reading them works well — Talk's dashboard API already returns title, time, location, calendar, attendee replies and attachments. Editing is where it falls apart: there is no way to reach this editor from another app, so a prototype either sends people to /apps/calendar/... and loses the view they were in, or rebuilds a small form and writes CalDAV itself. I tried both. Neither is something I would want to ship.
What I looked at before concluding that:
registerWidget('calendar_widget') mounts the real thing as a Vue component, but ReferenceProvider::matchReference() only accepts a whole calendar or a public share, not a single event
EditSimple takes only dark and lightBackdrop as props and reads the event from the Calendar store, so it cannot be handed one from outside
the package is "private": true with no main or exports, and there is no window.OCA.Calendar
So the gap is not "the editor cannot be extended" but "the editor cannot be placed". Those may well be the same API — an extension point that lets another app mount the editor on a given object id would cover both, and would let the host add its own action next to Save (in my case "Open conversation", which is the reason someone is in Talk rather than in Calendar).
If the interfaces stay minimal as discussed above, the piece I would need is small: mount the editor for one objectId (+ recurrenceId), and a way to know when it saved or closed. Happy to test against whatever shape you land on — I have the week view running on a Talk master build and can report back on what is missing in practice.
A parallel worth tracking: the Files app is getting the same treatment right now, driven by Teams.
nextcloud/server#63461 — "Make Files app reusable" — is a draft that lets another app render the Files view inside its own page. It resolves circles#2663 (show team-folder contents on the team page), and the Teams roadmap expects a team to come with a calendar, a Talk conversation and a collective, all surfaced in one place. So the same "render app A's view inside app B" need is arriving for Calendar from a second direction, independent of my Talk prototype.
The shape it landed on is small, which I think is good news for the "keep them minimal" line above:
embedded: true. When embedded, FilesList.vue renders a plain div where it would otherwise render NcAppContent — the host page owns the chrome, the embed owns only its own content. For the editor the equivalent would be rendering the form without Calendar's modal shell, so the host can supply its own dialog and put its own button next to Save.
A server-side bootstrap event.OCA\Files\Event\LoadFilesApp is dispatched by the host page to trigger the same script and initial-state loading the app's own controller does. Worth knowing that a JS-only API is not sufficient on its own; there is a PHP-side half.
An explicit teardown handle rather than a fire-and-forget mount, which is also what answers "how do I know when it closed".
One caveat, since it was raised in this thread: #63461 exposes this as window.OCP.Files.renderFilesApp, i.e. exactly the global-variable pattern that was called discouraged above and in the developer manual. If @nextcloud/calendar around window._nc_calendar_scope.v1_0 is where this is heading, that is the better half of the two patterns — I mention #63461 for the shape of the call, not for how it is published.
Concretely, what I would still need on top of the extension points discussed here is one entry point in the same spirit:
plus a way to observe saved/closed. Everything else — custom fields, validation, hiding built-in inputs — is already covered by the plugin API being designed above, and would apply to an embedded editor unchanged
Activity
ChristophWurst commentedon Jun 17, 2026
Proposed design: a framework-agnostic event editor plugin API
A concrete proposal for this ticket. The goal: let any Nextcloud app add custom inputs to the event editor and read/modify the event being edited — both the standard editor properties and arbitrary custom ICS properties — without depending on Calendar's internals or its frontend framework.
Scope (this iteration)
Design decisions
<your-tag>and assigns it a context object. The plugin can be written in vanilla JS, Lit, Svelte, React, Vue — anything that compiles to a custom element. No coupling to Calendar's Vue version. (Same mechanism the Files app uses for sidebar tabs.)windowglobal. Plugins never import Calendar code.X-<APPID>-…prefix but it is not enforced.Public API
Working package name (TBD — see open questions):
@nextcloud/calendar-plugin. Zero runtime deps; ships types + thin functions writing towindowglobals.EventComponentre-exported type-only from@nextcloud/calendar-js.Register a custom field
Calendar renders the element and assigns the context as a property (
el.context = ctx) — which is what lets us hand over a rich object with callbacks rather than just string attributes.Register a UI-less ICS hook
The context — centered on the live event model
eventaccessors map to the reactivecalendarObjectInstancestore state (read) and store actions (write) — the same path the built-in fields use, so everything stays consistent and reactive.getComponent()/get/setPropertymap to the raw calendar-jseventComponentfor anything the model doesn't cover.Recurrence semantics
The model is O(1) per open editor — it does not grow with occurrence count. Opening an instance resolves a single component via
getObjectAtRecurrenceId()→recurrenceManager.getOccurrenceAtExactly()(src/utils/calendarObject.js:34), projected into the single reactivecalendarObjectInstance. A weekly-forever series is stored as master VEVENT + a handful of RECURRENCE-ID exceptions; other occurrences are computed, never stored. So there is no per-occurrence plugin state.eventmodel andsetPropertyare scoped to the instance being edited, exactly like the built-in fields. For the common case ("add a field to this event"), a plugin needs zero recurrence awareness — the existing this-only / this-and-future save flow applies automatically (saveCalendarObjectInstance→eventComponent.createRecurrenceException(thisAndAllFuture),src/store/calendarObjectInstance.js:1519).primaryItem), sogetProperty()returns the master's value.getComponent()) is the escape hatch for genuinely cross-instance / series-level work: target the master while editing one occurrence (getComponent().primaryItem), enumerate the whole series/all exceptions (parentcalendarComponent), or touch RRULE/RECURRENCE-ID structure (not in the v1 model).event.instanceType/event.recurringand passes the chosen save scope intobeforeSave/serialize, so a plugin can decide series-vs-occurrence placement (and observe that this-and-future creates a brand-new master with a new UID).Runtime architecture (Calendar side)
register*functions write towindow._nc_calendar_editor_fields/_nc_calendar_serialize_hooksMaps and notifywindow.OCA.Calendarif it is already up. On boot, Calendar createswindow.OCA.Calendar(façade +version+ anEventTarget), drains the queues, and dispatchesregister:*for late registrations so an open editor updates live.EditFull.vue(bottom of the right column) and a compact one inEditSimple.vue(aboveSaveButtons,'simple'variants only). A small host-internal wrapper runsonInit(), creates each enabled element, assignsel.context, andwatches thecalendarObjectInstancestore to fire the context'schange— the reactivity bridge to the framework-agnostic element.saveCalendarObjectInstance(), beforeupdateCalendarObject(): emitbeforeSave, apply stagedsetPropertywrites, run allserializehooks on the (post-fork)eventComponent, thenmarkDirty()if anything changed (so theisDirty()gate does not skip the save).updateCalendarObject()then does the singletoICS()+ PUT (src/store/calendarObjects.js:107).Loading & timing (PHP)
rspack.config.js(Viewer-stylemain+init) sowindow.OCA.Calendarexists early.ViewControllerdispatches a newOCA\Calendar\Event\LoadAdditionalScriptsEventbefore render (mirrors the Files app). Plugin apps subscribe and call\OCP\Util::addInitScript('myapp', '…')→ runs before the editor mounts, and only on calendar pages.Correctness considerations
createRecurrenceException) splits at the component level and preserves unknown/X-*properties (worth a regression test). However, the Duplicate event action goes throughcopyCalendarObjectInstanceIntoEventComponent()(src/models/event.js:192, called atcalendarObjectInstance.js:1563), which copies only a whitelist and would drop custom properties. Fix: carry over all otherwise-unhandled properties there.summary,location, etc., a plugin and the user can touch the same field. Routing through store actions keeps state consistent (last-writer-wins, UI updates live). Consider a dev-mode warning.setProperty/getComponentcan overwrite RFC/core properties. Not enforced; docs recommend anX-<APPID>-…prefix.beforeSave.Worked example
A plugin that reads standard fields (attendees) and writes a derived custom property, re-rendering when the user edits the rest of the event:
Prior art
@nextcloud/viewer): framework-agnostic registration via a tiny npm package that writes handlers to awindow._oca_viewer_handlersMap queue, drained by the app on init (loaded viaaddInitScript).@nextcloud/files): sidebar tabs declare atagNameand register a Web Component (customElements.define) — the framework-agnostic content pattern — plus a reactivegetFilesRegistry()EventTarget for post-load registration.This proposal combines both: Viewer's queue + npm registration, Files' Web-Component content + reactive registry, plus a calendar-js serialize hook specific to the "influence ICS generation" requirement.
Open questions
api_package/(Viewer-style, versioned with the app) first, vs. a dedicated@nextcloud/calendar-pluginrepo now.@nextcloud/calendar-plugin,window.OCA.Calendar, the queue globals).variantsdefault —['full']only, or also'simple'(popover is space-constrained)?Rollout (focused, independently reviewable PRs)
copyCalendarObjectInstanceIntoEventComponent) + unit tests.window.OCA.Calendarfaçade + queue/registry + init entry point.eventmodel) inEditFull.vue/EditSimple.vue.beforeSavepass insaveCalendarObjectInstance.LoadAdditionalScriptsEvent.Assisted-by: ClaudeCode:claude-opus-4-8
odzhychko commentedon Jun 30, 2026
In the past I encountered, issues with web components bundling a custom Vue version.
It is something that Vue does not intend to support.
See vuejs/core#14067
In general libraries might break when bundled multiple times on a site.
Example Kotlin/kotlinx.coroutines#3874
But there surly exist solutions from this from practitioners of Micro Frontends.
So, adding a open question: Do we want to take care of isolation between plugins and the calendar app (and between plugins and plugins)? Or do we leave the responsibility to plugin authors.
odzhychko commentedon Jul 6, 2026
Currently multiple apps can already do load different versions of a library. For now we do plan to add further isolation.
odzhychko commentedon Jul 6, 2026
Input from @SebastianKrupinski
From the last two points I conclude, that we should not directly expose our model in the
this.ctx.So currently I strongly leaning on having a dedicated type for the data/methods we expose to the extension and fully encapsulate and keep our existing data structures internal.
The extension API can the also be independently versioned.
odzhychko commentedon Jul 6, 2026
Using a context property for custom components seems counterintuitive. I was directly thinking of plain element attributes (which can be reactively handled in custom components) and using events to communicate changes.
On the other hand, having the same context object/API for custom elements and hooks seems also good.
So I'm not sure about this one yet.
I will take a look at currently existing solutions in Nextcloud (e.g., extension in Viewer and Files) and other web projects.
odzhychko commentedon Jul 6, 2026
I got some use cases from a potential extension author:
(1) and (2) are not yet covered by the proposed extension points and would become custom extension points or some options, that can be dynamically set on
this.ctx.(2) and (4) can be covered by the custom elements because the extension author can display the validation result in custom components. But in the future, it might be interesting to have fine-grained hooks for our built-in components. Example for our attendee select field: Add custom logic to filter what attendees are allowed in a meeting and don't offer them to the user or highlight them as incompatible after the user selected them.
odzhychko commentedon Jul 13, 2026
Found an additional similar extension mechanism for
registerContactsMenuAction.odzhychko commentedon Jul 20, 2026
Found something similar again by chance.
Extension of settings with custom components:
https://github.com/nextcloud/recommendations/blob/f366d52feac013b418455e8306b6823b08307ae3/src/main.js#L38-L42
odzhychko commentedon Jul 20, 2026
@nextcloud/filesseems to have the most up-to-date approach for extensions.We can use the same approach for versioning as introduced in nextcloud-libraries/nextcloud-files#1492
Also notice, that exposing and using
window.OCA....was considered deprecated in nextcloud-libraries/nextcloud-files#1419Documented in https://docs.nextcloud.com/server/latest/developer_manual/digging_deeper/javascript-apis.html#global-variables as:
So for consistency we would build the npm package
@nextcloud/calendararoundwindow._nc_calendar_scope.v1_0for consistency.(And during initial phase around
window._nc_calendar_scope.v0_0Probably there are a few more interesting implementation details in
@nextcloud/filesworth locking closer into.odzhychko commentedon Aug 3, 2026
The editor extensions made by https://voxcloud.nl/roomvox/en/ (currently through patches) are an interesting use case for the editor extension API.
(Noticed while reviewing #8264)
odzhychko commentedon Aug 7, 2026
Another use case: Limit what type of recurrences is allowed. This is use full if events are synced with other systems that support less recurrence rules then CalDav standard/our editor.
Some example:
Implementing this with the current proposal would be do hide the repeat selector and add custom component to handle it.
If this becomes widely needed it might we could hide/restrict features behind capability checks that Nextcloud apps can toggle using the events editor API.
odzhychko commentedon Aug 21, 2026
Talked with @SebastianKrupinski about what methods for reading and modifying events to expose for extensions.
The gist is:
Summarized reasoning:
Rikdekker commentedon Sep 20, 2026
Another use case from the same corner, if it helps shape the scope: rendering the editor outside the Calendar app.
I have been prototyping a week view inside Talk that shows the events tied to your conversations. Reading them works well — Talk's dashboard API already returns title, time, location, calendar, attendee replies and attachments. Editing is where it falls apart: there is no way to reach this editor from another app, so a prototype either sends people to
/apps/calendar/...and loses the view they were in, or rebuilds a small form and writes CalDAV itself. I tried both. Neither is something I would want to ship.What I looked at before concluding that:
registerWidget('calendar_widget')mounts the real thing as a Vue component, butReferenceProvider::matchReference()only accepts a whole calendar or a public share, not a single eventEditSimpletakes onlydarkandlightBackdropas props and reads the event from the Calendar store, so it cannot be handed one from outside"private": truewith nomainorexports, and there is nowindow.OCA.CalendarSo the gap is not "the editor cannot be extended" but "the editor cannot be placed". Those may well be the same API — an extension point that lets another app mount the editor on a given object id would cover both, and would let the host add its own action next to Save (in my case "Open conversation", which is the reason someone is in Talk rather than in Calendar).
If the interfaces stay minimal as discussed above, the piece I would need is small: mount the editor for one
objectId(+recurrenceId), and a way to know when it saved or closed. Happy to test against whatever shape you land on — I have the week view running on a Talk master build and can report back on what is missing in practice.Rikdekker commentedon Sep 20, 2026
A parallel worth tracking: the Files app is getting the same treatment right now, driven by Teams.
nextcloud/server#63461 — "Make Files app reusable" — is a draft that lets another app render the Files view inside its own page. It resolves circles#2663 (show team-folder contents on the team page), and the Teams roadmap expects a team to come with a calendar, a Talk conversation and a collective, all surfaced in one place. So the same "render app A's view inside app B" need is arriving for Calendar from a second direction, independent of my Talk prototype.
The shape it landed on is small, which I think is good news for the "keep them minimal" line above:
Three details that seem relevant here:
embedded: true. When embedded,FilesList.vuerenders a plaindivwhere it would otherwise renderNcAppContent— the host page owns the chrome, the embed owns only its own content. For the editor the equivalent would be rendering the form without Calendar's modal shell, so the host can supply its own dialog and put its own button next to Save.OCA\Files\Event\LoadFilesAppis dispatched by the host page to trigger the same script and initial-state loading the app's own controller does. Worth knowing that a JS-only API is not sufficient on its own; there is a PHP-side half.One caveat, since it was raised in this thread: #63461 exposes this as
window.OCP.Files.renderFilesApp, i.e. exactly the global-variable pattern that was called discouraged above and in the developer manual. If@nextcloud/calendararoundwindow._nc_calendar_scope.v1_0is where this is heading, that is the better half of the two patterns — I mention #63461 for the shape of the call, not for how it is published.Concretely, what I would still need on top of the extension points discussed here is one entry point in the same spirit:
plus a way to observe saved/closed. Everything else — custom fields, validation, hiding built-in inputs — is already covered by the plugin API being designed above, and would apply to an embedded editor unchanged
odzhychko commentedon Sep 21, 2026
Rendering editor outside of calender would be cool.
#8714 is doing much work for it.