diff --git a/package-lock.json b/package-lock.json index 199462f54a..4d472db24b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@nextcloud/axios": "^2.5.2", "@nextcloud/calendar-availability-vue": "^3.0.0", "@nextcloud/calendar-js": "^8.1.6", - "@nextcloud/cdav-library": "^2.1.1", + "@nextcloud/cdav-library": "^2.2.0", "@nextcloud/dialogs": "^7.3.0", "@nextcloud/event-bus": "^3.3.3", "@nextcloud/initial-state": "^3.0.0", @@ -3675,16 +3675,16 @@ } }, "node_modules/@nextcloud/cdav-library": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@nextcloud/cdav-library/-/cdav-library-2.1.1.tgz", - "integrity": "sha512-pKjlfaqRGtUxcHvRsBJp0jLHxyl97sryOEBwXzi9qd+UBCbfd3GYO9EIhLBZTqU9LAKRmGPY3P5XQH2C3UxvSA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nextcloud/cdav-library/-/cdav-library-2.2.0.tgz", + "integrity": "sha512-vaikb6JFji8MjeNSl8rsbRm97sDI8f9+Qa4ixLtubYYomm6SuNxhTau7hVC+v7sIlzrBhQlz4h0Gmq9nEsZrug==", "license": "AGPL-3.0-or-later", "dependencies": { - "@nextcloud/axios": "^2.5.1" + "@nextcloud/axios": "^2.5.2" }, "engines": { - "node": "^20.0.0", - "npm": "^10.0.0" + "node": "^24.0.0", + "npm": "^11.3.0" } }, "node_modules/@nextcloud/dialogs": { @@ -4284,6 +4284,9 @@ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4306,6 +4309,9 @@ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4328,6 +4334,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4350,6 +4359,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4372,6 +4384,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4394,6 +4409,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ diff --git a/package.json b/package.json index 2d7eaf7704..2f1eabab3a 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@nextcloud/axios": "^2.5.2", "@nextcloud/calendar-availability-vue": "^3.0.0", "@nextcloud/calendar-js": "^8.1.6", - "@nextcloud/cdav-library": "^2.1.1", + "@nextcloud/cdav-library": "^2.2.0", "@nextcloud/dialogs": "^7.3.0", "@nextcloud/event-bus": "^3.3.3", "@nextcloud/initial-state": "^3.0.0", 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', 'Default reminder') }} + + + + {{ $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', + }, +}
+ {{ $t('calendar', 'This reminder will be automatically added to all new events created in this calendar') }} +