diff --git a/src/components/AppNavigation/EditCalendarModal.vue b/src/components/AppNavigation/EditCalendarModal.vue
index 9bd7f3d3d8..dcd2da5a90 100644
--- a/src/components/AppNavigation/EditCalendarModal.vue
+++ b/src/components/AppNavigation/EditCalendarModal.vue
@@ -43,6 +43,24 @@
{{ $t('calendar', 'Never show me as busy (set this calendar to transparent)') }}
+
+
+
+
+
+ {{ $t('calendar', 'This reminder will be automatically added to all new events created in this calendar') }}
+
+
+
{{ $t('calendar', 'Share calendar') }}
@@ -93,7 +111,7 @@
@@ -397,6 +523,25 @@ export default {
gap: 5px;
}
+ &__default-alarm {
+ margin-bottom: calc(var(--default-grid-baseline) * 2);
+
+ &__label {
+ display: block;
+ margin-bottom: var(--default-grid-baseline);
+ font-weight: bold;
+ }
+
+ &__select {
+ width: 100%;
+ }
+
+ &__hint {
+ margin-top: var(--default-grid-baseline);
+ color: var(--color-text-maxcontrast);
+ }
+ }
+
.checkbox-content {
margin-inline-start: 25px;
}
diff --git a/src/components/AppNavigation/Settings.vue b/src/components/AppNavigation/Settings.vue
index 876d894a23..fd92fa907b 100644
--- a/src/components/AppNavigation/Settings.vue
+++ b/src/components/AppNavigation/Settings.vue
@@ -338,10 +338,6 @@ export default {
return generateUrl('/settings/user/availability')
},
- nextcloudVersion() {
- return parseInt(OC.config.version.split('.')[0])
- },
-
defaultCalendarOptions() {
return this.calendarsStore.calendars
.filter((calendar) => !calendar.readOnly
@@ -350,7 +346,7 @@ export default {
},
/**
- * The default calendarci for incoming inivitations
+ * The default calendar for incoming inivitations
*
* @return {object|undefined} The default calendar or undefined if none is available
*/
diff --git a/src/components/AppointmentConfigModal.vue b/src/components/AppointmentConfigModal.vue
index 006fbf0d79..17e8fe08a7 100644
--- a/src/components/AppointmentConfigModal.vue
+++ b/src/components/AppointmentConfigModal.vue
@@ -172,6 +172,7 @@ import useAppointmentConfigsStore from '../store/appointmentConfigs.js'
import useCalendarsStore from '../store/calendars.js'
import useSettingsStore from '../store/settings.js'
import logger from '../utils/logger.js'
+import { isAfterVersion } from '@/utils/nextcloudVersion'
export default {
name: 'AppointmentConfigModal',
@@ -259,8 +260,7 @@ export default {
// TODO: Can be removed after NC version 30 support is dropped
availableCalendars() {
- const nextcloudMajorVersion = parseInt(window.OC.config.version.split('.')[0])
- if (nextcloudMajorVersion >= 31) {
+ if (isAfterVersion(31)) {
return this.sortedCalendars
}
return this.ownSortedCalendars
diff --git a/src/env.d.ts b/src/env.d.ts
index 6d2a59d4af..024d1231a5 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -4,10 +4,12 @@
*/
import type { getCSPNonce } from '@nextcloud/auth'
+import type { OC } from './types/oc.ts'
declare global {
let __webpack_nonce__: ReturnType
let __webpack_public_path__: string
+ const OC: OC
}
export {}
diff --git a/src/models/calendar.js b/src/models/calendar.js
index aec0ba750c..aaa34eeffa 100644
--- a/src/models/calendar.js
+++ b/src/models/calendar.js
@@ -3,6 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { detectColor, uidToHexColor } from '../utils/color.js'
+import { isAfterVersion } from '../utils/nextcloudVersion.ts'
import { mapDavShareeToCalendarShareObject } from './calendarShare.js'
/**
@@ -62,6 +63,8 @@ function getDefaultCalendarObject(props = {}) {
fetchedTimeRanges: [],
// Scheduling transparency
transparency: 'opaque',
+ // Default alarm/reminder for new events in seconds (null if disabled)
+ defaultAlarm: null,
...props,
}
}
@@ -103,6 +106,9 @@ function mapDavCollectionToCalendar(calendar, currentUserPrincipal) {
// then the default value CALDAV:opaque MUST be assumed.
// https://datatracker.ietf.org/doc/html/rfc6638#section-9.1
const transparency = calendar.transparency || 'opaque'
+ // Default alarm for new events in this calendar (in seconds)
+ // The value can be null or a number of seconds
+ const defaultAlarm = isAfterVersion(34) && calendar.defaultAlarm !== undefined ? calendar.defaultAlarm : null
let isSharedWithMe = false
if (!currentUserPrincipal) {
@@ -161,6 +167,7 @@ function mapDavCollectionToCalendar(calendar, currentUserPrincipal) {
shares,
timezone,
transparency,
+ defaultAlarm,
dav: calendar,
})
}
diff --git a/src/store/calendarObjectInstance.js b/src/store/calendarObjectInstance.js
index c235f66eb6..b18d1a561c 100644
--- a/src/store/calendarObjectInstance.js
+++ b/src/store/calendarObjectInstance.js
@@ -28,6 +28,7 @@ import {
getDateFromDateTimeValue,
} from '../utils/date.js'
import logger from '../utils/logger.js'
+import { isAfterVersion } from '../utils/nextcloudVersion.ts'
import { getBySetPositionAndBySetFromDate, getWeekDayFromDate } from '../utils/recurrence.js'
import useCalendarObjectsStore from './calendarObjects.js'
import useCalendarsStore from './calendars.js'
@@ -1391,9 +1392,18 @@ export default defineStore('calendarObjectInstance', {
const eventComponent = getObjectAtRecurrenceId(calendarObject, startDate)
const calendarObjectInstance = mapEventComponentToEventObject(eventComponent)
- // Add an alarm if the user set a default one in the settings. If
- // not, defaultReminder will not be a number (rather the string "none").
- const defaultReminder = parseInt(settingsStore.defaultReminder)
+ // Add an alarm if set. First check for calendar-specific default alarm (Nextcloud 34+),
+ // then fall back to the global default reminder setting.
+ const calendarsStore = useCalendarsStore()
+ const calendar = calendarsStore.getCalendarById(calendarObject.calendarId)
+
+ let defaultReminder = null
+ if (isAfterVersion(34) && calendar && calendar.defaultAlarm !== null) {
+ defaultReminder = parseInt(calendar.defaultAlarm)
+ } else {
+ defaultReminder = parseInt(settingsStore.defaultReminder)
+ }
+
if (!isNaN(defaultReminder)) {
this.addAlarmToCalendarObjectInstance({
calendarObjectInstance,
diff --git a/src/store/calendars.js b/src/store/calendars.js
index c59bff8f42..4673a9c847 100644
--- a/src/store/calendars.js
+++ b/src/store/calendars.js
@@ -27,6 +27,7 @@ import getTimezoneManager from '../services/timezoneDataProviderService.js'
import { uidToHexColor } from '../utils/color.js'
import { dateFactory, getUnixTimestampFromDate } from '../utils/date.js'
import logger from '../utils/logger.js'
+import { isAfterVersion } from '../utils/nextcloudVersion.ts'
import useCalendarObjectsStore from './calendarObjects.js'
import useFetchedTimeRangesStore from './fetchedTimeRanges.js'
import useImportFilesStore from './importFiles.js'
@@ -586,6 +587,29 @@ export default defineStore('calendars', {
this.calendarsById[calendar.id].transparency = transparency
},
+ /**
+ * Change a calendar's default alarm
+ *
+ * @param {object} data destructuring object
+ * @param {object} data.calendar the calendar to modify
+ * @param {string|null} data.defaultAlarm the new default alarm in seconds (or null to disable)
+ * @return {Promise}
+ */
+ async changeCalendarDefaultAlarm({ calendar, defaultAlarm }) {
+ if (!isAfterVersion(34)) {
+ return
+ }
+
+ if (calendar.dav.defaultAlarm === defaultAlarm) {
+ return
+ }
+
+ calendar.dav.defaultAlarm = defaultAlarm
+
+ await calendar.dav.update()
+ this.calendarsById[calendar.id].defaultAlarm = defaultAlarm
+ },
+
/**
* Share calendar with User or Group
*
diff --git a/src/types/oc.ts b/src/types/oc.ts
new file mode 100644
index 0000000000..c0e3160929
--- /dev/null
+++ b/src/types/oc.ts
@@ -0,0 +1,8 @@
+// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+export type OC = {
+ config: {
+ version: string
+ }
+}
diff --git a/src/utils/nextcloudVersion.ts b/src/utils/nextcloudVersion.ts
new file mode 100644
index 0000000000..b41ae10945
--- /dev/null
+++ b/src/utils/nextcloudVersion.ts
@@ -0,0 +1,22 @@
+/**
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/**
+ * Get the current Nextcloud major version
+ *
+ * @return The major version number
+ */
+export function getNextcloudVersion(): number {
+ return parseInt(OC.config.version.split('.')[0])
+}
+
+/**
+ * Whether the current Nextcloud version is equal or higher than the given version
+ *
+ * @return True if supported
+ */
+export function isAfterVersion(version: number): boolean {
+ return getNextcloudVersion() >= version
+}
diff --git a/tests/javascript/unit/models/calendar.test.js b/tests/javascript/unit/models/calendar.test.js
index 8c25ba382f..164747461b 100644
--- a/tests/javascript/unit/models/calendar.test.js
+++ b/tests/javascript/unit/models/calendar.test.js
@@ -42,6 +42,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
transparency: 'opaque',
+ defaultAlarm: null,
})
})
@@ -76,6 +77,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
transparency: 'opaque',
+ defaultAlarm: null,
})
})
@@ -125,6 +127,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0)
@@ -176,6 +179,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0)
@@ -225,6 +229,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0)
@@ -274,6 +279,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0)
@@ -323,6 +329,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0)
@@ -372,6 +379,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0)
@@ -421,6 +429,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0)
@@ -470,6 +479,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0)
@@ -575,6 +585,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(4)
@@ -696,6 +707,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
expect(mapDavShareeToCalendarShareObject).toHaveBeenCalledTimes(0)
@@ -746,6 +758,7 @@ describe('Test suite: Calendar model (models/calendar.js)', () => {
calendarObjects: [],
fetchedTimeRanges: [],
loading: false,
+ defaultAlarm: null,
})
})
diff --git a/tests/javascript/unit/setup.js b/tests/javascript/unit/setup.js
index 5dc5774907..f9ea4fb52d 100644
--- a/tests/javascript/unit/setup.js
+++ b/tests/javascript/unit/setup.js
@@ -4,3 +4,9 @@
*/
document.title = 'Standard Nextcloud title'
+
+globalThis.OC = {
+ config: {
+ version: '34.0.0',
+ },
+}