Skip to content
Open
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
147 changes: 146 additions & 1 deletion src/components/AppNavigation/EditCalendarModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@
{{ $t('calendar', 'Never show me as busy (set this calendar to transparent)') }}
</NcCheckboxRadioSwitch>
</template>
<template v-if="!calendar.isSharedWithMe && isAfterVersion">
<div class="edit-calendar-modal__default-alarm">
<label for="default-alarm-select" class="edit-calendar-modal__default-alarm__label">
{{ $t('calendar', 'Default reminder') }}
</label>
<NcSelect
id="default-alarm-select"
v-model="selectedDefaultAlarm"
:options="defaultAlarmOptions"
:clearable="false"
:placeholder="$t('calendar', 'Select default reminder')"
class="edit-calendar-modal__default-alarm__select"
@update:modelValue="defaultAlarmChanged = true" />
<p class="edit-calendar-modal__default-alarm__hint">
{{ $t('calendar', 'This reminder will be automatically added to all new events created in this calendar') }}
</p>
</div>
</template>
<template v-if="canBeShared">
<h3 class="edit-calendar-modal__sharing-header">
{{ $t('calendar', 'Share calendar') }}
Expand Down Expand Up @@ -93,7 +111,7 @@
<script>
import { showError } from '@nextcloud/dialogs'
import { getLanguage } from '@nextcloud/l10n'
import { NcAppNavigationSpacer, NcButton, NcCheckboxRadioSwitch, NcColorPicker, NcModal, NcTextField } from '@nextcloud/vue'
import { NcAppNavigationSpacer, NcButton, NcCheckboxRadioSwitch, NcColorPicker, NcModal, NcSelect, NcTextField } from '@nextcloud/vue'
import { mapStores } from 'pinia'
import CheckIcon from 'vue-material-design-icons/Check.vue'
import CloseIcon from 'vue-material-design-icons/Close.vue'
Expand All @@ -103,8 +121,16 @@ import InternalLink from './EditCalendarModal/InternalLink.vue'
import PublishCalendar from './EditCalendarModal/PublishCalendar.vue'
import ShareItem from './EditCalendarModal/ShareItem.vue'
import SharingSearch from './EditCalendarModal/SharingSearch.vue'
import { getDefaultAlarms } from '../../defaults/defaultAlarmProvider.js'
import alarmFormat from '../../filters/alarmFormat.js'
import useCalendarsStore from '../../store/calendars.js'
import useSettingsStore from '../../store/settings.js'
import {
getAmountAndUnitForTimedEvents,
getAmountHoursMinutesAndUnitForAllDayEvents,
} from '../../utils/alarms.js'
import logger from '../../utils/logger.js'
import { isAfterVersion } from '../../utils/nextcloudVersion.ts'

export default {
name: 'EditCalendarModal',
Expand All @@ -113,6 +139,7 @@ export default {
NcColorPicker,
NcButton,
NcTextField,
NcSelect,
PublishCalendar,
SharingSearch,
ShareItem,
Expand All @@ -132,6 +159,8 @@ export default {
isTransparent: false,
calendarName: undefined,
calendarNameChanged: false,
selectedDefaultAlarm: null,
defaultAlarmChanged: false,
}
},

Expand Down Expand Up @@ -220,6 +249,45 @@ export default {
{ types: localizedTypes },
)
},

/**
* Get the default alarm options for the select dropdown
*
* @return {Array}
*/
defaultAlarmOptions() {
const settingsStore = useSettingsStore()
const currentUserTimezone = settingsStore.getResolvedTimezone
const locale = settingsStore.momentLocale

const options = [
{
label: this.$t('calendar', 'None'),
value: null,
},
]

// Add standard alarm options for timed events
const alarms = getDefaultAlarms(false)
for (const alarm of alarms) {
const alarmObject = this.getAlarmObjectFromTriggerTime(alarm)
options.push({
label: alarmFormat(alarmObject, false, currentUserTimezone, locale),
value: alarm,
})
}

return options
},

/**
* Whether the default alarm feature is supported (Nextcloud 34+)
*
* @return {boolean}
*/
isAfterVersion() {
return isAfterVersion(34)
},
},

watch: {
Expand All @@ -233,6 +301,16 @@ export default {
this.calendarNameChanged = false
this.calendarColorChanged = false
this.isTransparent = calendar.transparency === 'transparent'

// Initialize default alarm
if (calendar.defaultAlarm === null) {
this.selectedDefaultAlarm = this.defaultAlarmOptions[0]
} else {
const value = parseInt(calendar.defaultAlarm)
const option = this.defaultAlarmOptions.find((opt) => opt.value === value)
this.selectedDefaultAlarm = option || this.defaultAlarmOptions[0]
}
this.defaultAlarmChanged = false
},
},

Expand Down Expand Up @@ -298,6 +376,25 @@ export default {
}
},

/**
* Save the calendar default alarm.
*/
async saveDefaultAlarm() {
try {
const defaultAlarmValue = this.selectedDefaultAlarm ? this.selectedDefaultAlarm.value : null
await this.calendarsStore.changeCalendarDefaultAlarm({
calendar: this.calendar,
defaultAlarm: defaultAlarmValue,
})
} catch (error) {
logger.error('Failed to save calendar default alarm', {
calendar: this.calendar,
defaultAlarm: this.selectedDefaultAlarm,
})
throw error
}
},

/**
* Save unsaved changes and close the modal.
*
Expand All @@ -315,6 +412,9 @@ export default {
if (this.calendarNameChanged) {
await this.saveName()
}
if (this.isAfterVersion && this.defaultAlarmChanged) {
await this.saveDefaultAlarm()
}
} catch (error) {
showError(this.$t('calendar', 'Failed to save calendar name and color'))
}
Expand All @@ -331,6 +431,32 @@ export default {
})
this.closeModal()
},

/**
* Create alarm object from trigger time for formatting
*
* @param {number} time Total amount of seconds for the trigger
* @return {object} The alarm object
*/
getAlarmObjectFromTriggerTime(time) {
const timedData = getAmountAndUnitForTimedEvents(time)
const allDayData = getAmountHoursMinutesAndUnitForAllDayEvents(time)

return {
isRelative: true,
absoluteDate: null,
absoluteTimezoneId: null,
relativeIsBefore: time < 0,
relativeIsRelatedToStart: true,
relativeUnitTimed: timedData.unit,
relativeAmountTimed: timedData.amount,
relativeUnitAllDay: allDayData.unit,
relativeAmountAllDay: allDayData.amount,
relativeHoursAllDay: allDayData.hours,
relativeMinutesAllDay: allDayData.minutes,
relativeTrigger: time,
}
},
},
}
</script>
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 1 addition & 5 deletions src/components/AppNavigation/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
*/
Expand Down
4 changes: 2 additions & 2 deletions src/components/AppointmentConfigModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
*/

import type { getCSPNonce } from '@nextcloud/auth'
import type { OC } from './types/oc.ts'

declare global {
let __webpack_nonce__: ReturnType<typeof getCSPNonce>
let __webpack_public_path__: string
const OC: OC
}

export {}
7 changes: 7 additions & 0 deletions src/models/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -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,
}
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -161,6 +167,7 @@ function mapDavCollectionToCalendar(calendar, currentUserPrincipal) {
shares,
timezone,
transparency,
defaultAlarm,
dav: calendar,
})
}
Expand Down
16 changes: 13 additions & 3 deletions src/store/calendarObjectInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions src/store/calendars.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
*
Expand Down
8 changes: 8 additions & 0 deletions src/types/oc.ts
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Loading