Skip to content
Merged
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
5 changes: 3 additions & 2 deletions css/app-settings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@
display: block;
}
}

&--timezone {

&--timezone,
&--default-calendar {
width: 100%;

.multiselect {
Expand Down
74 changes: 73 additions & 1 deletion src/components/AppNavigation/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
-->

<template>
<AppNavigationSettings exclude-click-outside-classes="import-modal"
<AppNavigationSettings :exclude-click-outside-selectors="['.vs__dropdown-menu', '.modal-wrapper']"
:name="settingsTitle">
<ul class="settings-fieldset-interior">
<SettingsImportSection :is-disabled="loadingCalendars" />
Expand Down Expand Up @@ -71,6 +71,18 @@
label="label"
@option:selected="changeSlotDuration" />
</li>
<!-- TODO: remove version check once Nextcloud 28 is not supported anymore -->
<li v-if="currentUserPrincipal && defaultCalendarOptions.length > 1 && nextcloudVersion >= 29"
class="settings-fieldset-interior-item settings-fieldset-interior-item--default-calendar">
<label :for="defaultCalendarPickerId">
{{ $t('calendar', 'Default calendar for invitations and new events') }}
</label>
<CalendarPicker :value="defaultCalendar"
:calendars="defaultCalendarOptions"
:disabled="savingDefaultCalendarId"
:input-id="defaultCalendarPickerId"
@select-calendar="changeDefaultCalendar" />
</li>
<li class="settings-fieldset-interior-item settings-fieldset-interior-item--defaultReminder">
<label for="defaultReminder">{{ $t('calendar', 'Default reminder') }}</label>
<NcSelect :id="defaultReminder"
Expand Down Expand Up @@ -124,6 +136,8 @@ import {
NcAppNavigationSettings as AppNavigationSettings,
NcSelect,
} from '@nextcloud/vue'
import CalendarPicker from '../Shared/CalendarPicker.vue'

import {
generateRemoteUrl,
generateUrl,
Expand Down Expand Up @@ -156,6 +170,9 @@ import ClipboardArrowLeftOutline from 'vue-material-design-icons/ClipboardArrowL
import InformationVariant from 'vue-material-design-icons/InformationVariant.vue'
import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue'

import logger from '../../utils/logger.js'
import { randomId } from '../../utils/randomId.js'

export default {
name: 'Settings',
components: {
Expand All @@ -171,6 +188,7 @@ export default {
ClipboardArrowLeftOutline,
InformationVariant,
OpenInNewIcon,
CalendarPicker,
},
props: {
loadingCalendars: {
Expand All @@ -186,14 +204,18 @@ export default {
savingPopover: false,
savingSlotDuration: false,
savingDefaultReminder: false,
savingDefaultCalendarId: false,
savingWeekend: false,
savingWeekNumber: false,
savingDefaultCalendar: false,
displayKeyboardShortcuts: false,
defaultCalendarPickerId: randomId(),
}
},
computed: {
...mapGetters({
birthdayCalendar: 'hasBirthdayCalendar',
currentUserPrincipal: 'getCurrentUserPrincipal',
}),
...mapState({
eventLimit: state => state.settings.eventLimit,
Expand Down Expand Up @@ -271,6 +293,28 @@ export default {
nextcloudVersion() {
return parseInt(OC.config.version.split('.')[0])
},
defaultCalendarOptions() {
return this.$store.state.calendars.calendars
.filter(calendar => !calendar.readOnly && !calendar.isSharedWithMe)
},
/**
* The default calendar for new events and inivitations
*
* @return {object|undefined} The default calendar or undefined if none is available
*/
defaultCalendar() {
const defaultCalendarUrl = this.currentUserPrincipal.scheduleDefaultCalendarUrl
const calendar = this.defaultCalendarOptions
.find(calendar => calendar.url === defaultCalendarUrl)

// If the default calendar is not or no longer available,
// pick the first calendar in the list of available calendars.
if (!calendar) {
return this.defaultCalendarOptions[0]
}

return calendar
},
},
methods: {
async toggleBirthdayEnabled() {
Expand Down Expand Up @@ -396,6 +440,34 @@ export default {
this.savingDefaultReminder = false
}
},
/**
* Changes the default calendar for new events
*
* @param {object} selectedCalendar The new selected default calendar
*/
async changeDefaultCalendar(selectedCalendar) {
if (!selectedCalendar) {
return
}

this.savingDefaultCalendar = true

try {
await this.$store.dispatch('changePrincipalScheduleDefaultCalendarUrl', {
principal: this.currentUserPrincipal,
scheduleDefaultCalendarUrl: selectedCalendar.url,
})
} catch (error) {
logger.error('Error while changing default calendar', {
error,
calendarUrl: selectedCalendar.url,
selectedCalendar,
})
showError(this.$t('calendar', 'Failed to save default calendar'))
} finally {
this.savingDefaultCalendar = false
}
},
/**
* Copies the primary CalDAV url to the user's clipboard.
*/
Expand Down
13 changes: 11 additions & 2 deletions src/components/Shared/CalendarPicker.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<NcSelect label="id"
input-id="url"
:input-id="inputId"
:disabled="isDisabled"
:options="options"
:value="valueIds"
Expand All @@ -25,6 +25,7 @@
<script>
import { NcSelect } from '@nextcloud/vue'
import CalendarPickerOption from './CalendarPickerOption.vue'
import { randomId } from '../../utils/randomId.js'

export default {
name: 'CalendarPicker',
Expand All @@ -49,12 +50,20 @@ export default {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
inputId: {
type: String,
default: () => randomId(),
},
},
computed: {
isDisabled() {
// for pickers where multiple can be selected (zero or more) we don't want to disable the picker
// for calendars where only one calendar can be selected, disable if there are < 2
return this.multiple ? this.calendars.length < 1 : this.calendars.length < 2
return this.disabled || (this.multiple ? this.calendars.length < 1 : this.calendars.length < 2)
},
valueIds() {
if (Array.isArray(this.value)) {
Expand Down
4 changes: 4 additions & 0 deletions src/models/principal.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ const getDefaultPrincipalObject = (props) => Object.assign({}, {
isCalendarRoom: false,
// The id of the principal without prefix. e.g. userId / groupId / etc.
principalId: null,
// The url of the default calendar for invitations
scheduleDefaultCalendarUrl: null,
}, props)

/**
Expand All @@ -80,6 +82,7 @@ const mapDavToPrincipal = (dav) => {
const emailAddress = dav.email

const displayname = dav.displayname
const scheduleDefaultCalendarUrl = dav.scheduleDefaultCalendarUrl

const isUser = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_USER)
const isGroup = dav.principalScheme.startsWith(PRINCIPAL_PREFIX_GROUP)
Expand Down Expand Up @@ -118,6 +121,7 @@ const mapDavToPrincipal = (dav) => {
isCalendarRoom,
principalId,
userId,
scheduleDefaultCalendarUrl,
})
}

Expand Down
9 changes: 7 additions & 2 deletions src/store/calendarObjects.js
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,13 @@ const actions = {
vObject.undirtify()
}

const firstCalendar = context.getters.sortedCalendars[0].id
return Promise.resolve(mapCalendarJsToCalendarObject(calendar, firstCalendar))
const defaultCalendarUrl = context.getters.getCurrentUserPrincipal.scheduleDefaultCalendarUrl
const defaultCalendar = context.getters.getCalendarByUrl(defaultCalendarUrl)

return Promise.resolve(mapCalendarJsToCalendarObject(
calendar,
defaultCalendar?.id ?? context.getters.sortedCalendars[0].id,
))
},

/**
Expand Down
8 changes: 8 additions & 0 deletions src/store/calendars.js
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,14 @@ const getters = {
*/
getCalendarById: (state) => (calendarId) => state.calendarsById[calendarId],

/**
* Gets a calendar by its url
*
* @param {object} state the store data
* @return {function({String}): {Object}}
*/
getCalendarByUrl: (state) => (url) => state.calendars.find((calendar) => calendar.url === url),

/**
* Gets the contact's birthday calendar or null
*
Expand Down
35 changes: 35 additions & 0 deletions src/store/principals.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ const mutations = {
setCurrentUserPrincipal(state, { principalId }) {
state.currentUserPrincipal = principalId
},

/**
* Changes the schedule-default-calendar-URL of a principal
*
* @param {object} state The vuex state
* @param {object} data The destructuring object
* @param {object} data.principal The principal to modify
* @param {string} data.scheduleDefaultCalendarUrl The new schedule-default-calendar-URL
*/
changePrincipalScheduleDefaultCalendarUrl(state, { principal, scheduleDefaultCalendarUrl }) {
Vue.set(
state.principalsById[principal.id],
'scheduleDefaultCalendarUrl',
scheduleDefaultCalendarUrl,
)
},
}

const getters = {
Expand Down Expand Up @@ -147,6 +163,25 @@ const actions = {
context.commit('setCurrentUserPrincipal', { principalId: principal.id })
logger.debug(`Current user principal is ${principal.url}`)
},

/**
* Change a principal's schedule-default-calendar-URL
*
* @param {object} context The vuex context
* @param {object} data The destructuring object
* @param {object} data.principal The principal to modify
* @param {string} data.scheduleDefaultCalendarUrl The new schedule-default-calendar-URL
* @return {Promise<void>}
*/
async changePrincipalScheduleDefaultCalendarUrl(context, { principal, scheduleDefaultCalendarUrl }) {
principal.dav.scheduleDefaultCalendarUrl = scheduleDefaultCalendarUrl

await principal.dav.update()
context.commit('changePrincipalScheduleDefaultCalendarUrl', {
principal,
scheduleDefaultCalendarUrl,
})
},
}

export default { state, mutations, getters, actions }
2 changes: 2 additions & 0 deletions tests/javascript/unit/models/principal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe('Test suite: Principal model (models/principal.js)', () => {
isCalendarResource: false,
isCalendarRoom: false,
principalId: null,
scheduleDefaultCalendarUrl: null,
})
})

Expand All @@ -63,6 +64,7 @@ describe('Test suite: Principal model (models/principal.js)', () => {
isCalendarRoom: false,
principalId: 'bar',
otherProp: 'foo',
scheduleDefaultCalendarUrl: null,
})
})

Expand Down