Merge pull request #862 from nextcloud/fix/467

Prevent editing not public tasks in lists shared with me
This commit is contained in:
Raimund Schlüßler 2020-02-17 10:49:18 +01:00 committed by GitHub
commit f58e8bd12f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 472 additions and 212 deletions

View file

@ -1,85 +0,0 @@
/**
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
* @author Team Popcorn <teampopcornberlin@gmail.com>
* @author Raimund Schlüßler <raimund.schluessler@mailbox.org>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
.calendar-sharee {
padding: 0 5px;
display: inline-flex;
align-items: center;
width: 100%;
.icon {
margin-right: 5px;
opacity: 0.2;
width: 16px;
height: 16px;
display: inline-block;
margin-bottom: 2px;
&.icon-loading-small {
opacity: 1;
}
}
&__identifier {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
vertical-align: top;
opacity: 0.7;
}
&__utils {
padding: 0 !important;
float: right;
position: relative !important;
display: inline-flex;
align-items: center;
flex-shrink: 0;
height: 20px;
.icon-delete {
display: inline-block;
width: 20px;
height: 20px;
opacity: 0.4;
margin-bottom: 2px;
margin-left: 4px;
&:hover {
box-shadow: unset !important;
}
}
// loading state
&--disabled {
opacity: .2 !important;
}
.checkbox + label {
padding: 0 !important;
}
label {
opacity: 0.7;
}
}
}

View file

@ -23,40 +23,49 @@
*/
.calendar-shares {
width: calc(100% - 6px);
margin: 6px;
&__list {
margin-top: 8px;
margin-bottom: 12px;
display: flex;
flex-direction: column;
}
&__shareematch--bold {
font-weight: bold;
}
.app-navigation-entry {
padding-left: 0 !important;
.avatar {
width: 32px;
height: 32px;
background-color: var(--color-border-dark);
background-size: 16px;
}
&__utils {
.action-checkbox__label {
padding-right: 0 !important;
}
.icon-loading::after {
top: 70%;
left: 95%;
height: 14px;
width: 14px;
}
.multiselect {
width: inherit;
margin: 0;
.multiselect__tags:focus-within,
.multiselect__tags:hover {
border-color: var(--color-primary-element);
.action-checkbox__label::before {
margin: 4px 4px 0 !important;
}
}
&__multiselect {
padding-left: 6px !important;
&:not(.showContent) .multiselect__content-wrapper {
display: none;
}
.multiselect {
width: calc(100% - 14px);
margin: 0;
.multiselect__tags:focus-within,
.multiselect__tags:hover {
border-color: var(--color-primary-element);
}
.multiselect__content-wrapper {
z-index: 101 !important;
&:not(.showContent) .multiselect__content-wrapper {
display: none;
}
.multiselect__content-wrapper {
z-index: 101 !important;
}
}
}
}
}

View file

@ -27,6 +27,11 @@
display: none;
}
}
.app-navigation-entry__utils .icon-loading {
height: 32px;
width: 32px;
}
}
&.edit {

View file

@ -27,4 +27,3 @@
@import './src/Calendars/Calendar.scss';
@import './src/Calendars/CalendarShares.scss';
@import './src/Calendars/CalendarSharee.scss';

View file

@ -61,27 +61,34 @@ export default {
calendars: state => state.calendars.calendars,
}),
},
beforeMount() {
async beforeMount() {
// get calendars then get tasks
client.connect({ enableCalDAV: true }).then(() => {
this.$store.dispatch('getCalendars')
.then((calendars) => {
// No calendars? Create a new one!
if (calendars.length === 0) {
let color = '#0082C9'
if (this.$OCA.Theming) {
color = this.$OCA.Theming.color
}
this.$store.dispatch('appendCalendar', { displayName: this.$t('tasks', 'Tasks'), color })
.then(() => {
this.fetchTasks()
})
// else, let's get those tasks!
} else {
this.fetchTasks()
}
})
await client.connect({ enableCalDAV: true })
await this.$store.dispatch('fetchCurrentUserPrincipal')
const calendars = await this.$store.dispatch('getCalendars')
const owners = []
calendars.forEach((calendar) => {
if (owners.indexOf(calendar.owner) === -1) {
owners.push(calendar.owner)
}
})
owners.forEach((owner) => {
this.$store.dispatch('fetchPrincipalByUrl', {
url: owner,
})
})
// No calendars? Create a new one!
if (calendars.length === 0) {
let color = '#0082C9'
if (this.$OCA.Theming) {
color = this.$OCA.Theming.color
}
await this.$store.dispatch('appendCalendar', { displayName: this.$t('tasks', 'Tasks'), color })
this.fetchTasks()
// else, let's get those tasks!
} else {
this.fetchTasks()
}
},
methods: {
/**

View file

@ -23,23 +23,25 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<template>
<div class="calendar-shares">
<Multiselect
id="users-groups-search"
:options="usersOrGroups"
:searchable="true"
:internal-search="false"
:max-height="600"
:show-no-results="true"
:placeholder="placeholder"
:class="{ 'showContent': inputGiven, 'icon-loading': isLoading }"
:user-select="true"
open-direction="bottom"
track-by="user"
label="user"
@search-change="findSharee"
@input="shareCalendar" />
<!-- list of user or groups calendar is shared with -->
<ul v-if="calendar.shares.length > 0" class="calendar-shares__list">
<ul>
<li class="app-navigation-entry__multiselect">
<Multiselect
id="users-groups-search"
:options="usersOrGroups"
:searchable="true"
:internal-search="false"
:max-height="600"
:show-no-results="true"
:placeholder="placeholder"
:class="{ 'showContent': inputGiven, 'icon-loading': isLoading }"
:user-select="true"
open-direction="bottom"
track-by="user"
label="user"
@search-change="findSharee"
@input="shareCalendar" />
</li>
<!-- list of user or groups calendar is shared with -->
<CalendarSharee v-for="sharee in calendar.shares"
:key="sharee.uri"
:sharee="sharee"

View file

@ -4,6 +4,9 @@
@author Team Popcorn <teampopcornberlin@gmail.com>
@author Raimund Schlüßler <raimund.schluessler@mailbox.org>
@copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com>
@author Georg Ehrke <oc.list@georgehrke.com>
@license GNU AGPL version 3 or any later version
This program is free software: you can redistribute it and/or modify
@ -22,40 +25,51 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<li class="calendar-sharee">
<span :class="{
'icon-loading-small': loading,
'icon-group': sharee.isGroup && !loading,
'icon-user': !sharee.isGroup && !loading
}"
class="icon" />
<span class="calendar-sharee__identifier">
{{ sharee.displayName }}
</span>
<span class="calendar-sharee__utils">
<input
:id="uid"
:checked="writeable"
<AppNavigationItem
:title="sharee.displayName">
<template slot="icon">
<div v-if="sharee.isGroup" class="avatar icon-group" />
<div v-else-if="sharee.isCircle" class="avatar icon-circle" />
<Avatar v-else
:user="sharee.id"
:display-name="sharee.displayName"
:disable-menu="true" />
</template>
<template slot="counter">
<ActionCheckbox
:disabled="loading"
class="checkbox"
name="editable"
type="checkbox"
@change="editSharee">
<label :for="uid">
:checked="writeable"
@update:checked="editSharee">
{{ $t('tasks', 'can edit') }}
</label>
<a :class="{'calendar-sharee__utils--disabled': loading}"
href="#"
title="Delete"
class="icon-delete"
@click="deleteSharee" />
</span>
</li>
</ActionCheckbox>
</template>
<template slot="actions">
<ActionButton
icon="icon-delete"
:disabled="loading"
@click.prevent.stop="deleteSharee">
{{ $t('tasks', 'Unshare with {displayName}', { displayName: sharee.displayName }) }}
</ActionButton>
</template>
</AppNavigationItem>
</template>
<script>
import { ActionButton } from '@nextcloud/vue/dist/Components/ActionButton'
import { ActionCheckbox } from '@nextcloud/vue/dist/Components/ActionCheckbox'
import { AppNavigationItem } from '@nextcloud/vue/dist/Components/AppNavigationItem'
import { Avatar } from '@nextcloud/vue/dist/Components/Avatar'
export default {
name: 'CalendarSharee',
components: {
ActionButton,
ActionCheckbox,
AppNavigationItem,
Avatar,
},
props: {
calendar: {
type: Object,

View file

@ -37,18 +37,18 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
<AppNavigationIconBullet slot="icon" :color="calendar.color" />
<template v-if="!deleteTimeout" slot="counter">
<AppNavigationCounter>
{{ calendarCount(calendar.id) | counterFormatter }}
</AppNavigationCounter>
<Actions v-if="!calendar.readOnly">
<Actions v-if="calendar.canBeShared">
<ActionButton
:icon="sharingIconClass"
@click="toggleShare">
{{ sharedWithTooltip }}
</ActionButton>
</Actions>
<Avatar v-if="calendar.isSharedWithMe && calendar.loadedOwnerPrincipal" :user="calendar.ownerUserId" :display-name="calendar.ownerDisplayname" />
<div v-if="calendar.isSharedWithMe && !calendar.loadedOwnerPrincipal" class="icon icon-loading" />
<Avatar v-if="calendar.isSharedWithMe && loadedOwnerPrincipal" :user="ownerUserId" :display-name="ownerDisplayname" />
<div v-if="calendar.isSharedWithMe && !loadedOwnerPrincipal" class="icon icon-loading" />
<AppNavigationCounter v-if="calendarCount">
{{ calendarCount | counterFormatter }}
</AppNavigationCounter>
</template>
<template v-if="!deleteTimeout" slot="actions">
@ -75,13 +75,12 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
{{ $t('tasks', 'Download') }}
</ActionLink>
<ActionButton
v-if="!calendar.readOnly"
v-tooltip="{
placement: 'left',
boundariesElement: 'body',
content: deleteMessage
}"
icon="icon-delete"
:icon="calendar.isSharedWithMe ? 'icon-close' : 'icon-delete'"
@click="scheduleDelete">
{{ !calendar.isSharedWithMe ? $t('tasks', 'Delete') : $t('tasks', 'Unshare') }}
</ActionButton>
@ -91,7 +90,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
<ActionButton
icon="icon-history"
@click.prevent.stop="cancelDelete">
{{ $n('tasks', 'Deleting the calendar in {countdown} second', 'Deleting the calendar in {countdown} seconds', countdown, { countdown }) }}
{{ undoDeleteMessage }}
</ActionButton>
</template>
@ -130,6 +129,7 @@ import Colorpicker from './Colorpicker'
import ShareCalendar from './CalendarShare'
import ClickOutside from 'vue-click-outside'
import Avatar from '@nextcloud/vue/dist/Components/Avatar'
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
import AppNavigationCounter from '@nextcloud/vue/dist/Components/AppNavigationCounter'
import AppNavigationIconBullet from '@nextcloud/vue/dist/Components/AppNavigationIconBullet'
@ -143,6 +143,7 @@ export default {
components: {
Colorpicker,
ShareCalendar,
Avatar,
AppNavigationItem,
AppNavigationCounter,
AppNavigationIconBullet,
@ -190,16 +191,26 @@ export default {
},
computed: {
...mapGetters({
calendarCount: 'getCalendarCount',
getCalendarCount: 'getCalendarCount',
isCalendarNameUsed: 'isCalendarNameUsed',
getTask: 'getTaskByUri',
getPrincipalByUrl: 'getPrincipalByUrl',
}),
calendarCount() {
return this.getCalendarCount(this.calendar.id)
},
deleteMessage() {
return !this.calendar.isSharedWithMe
? this.$t('tasks', 'This will delete the calendar "{calendar}" and all corresponding events and tasks.', { calendar: this.calendar.displayName })
: this.$t('tasks', 'This will unshare the calendar "{calendar}".', { calendar: this.calendar.displayName })
},
undoDeleteMessage() {
return !this.calendar.isSharedWithMe
? this.$n('tasks', 'Deleting the calendar in {countdown} second', 'Deleting the calendar in {countdown} seconds', this.countdown, { countdown: this.countdown })
: this.$n('tasks', 'Unsharing the calendar in {countdown} second', 'Unsharing the calendar in {countdown} seconds', this.countdown, { countdown: this.countdown })
},
sharingIconClass() {
if (this.calendar.shares.length) {
return 'icon-shared'
@ -233,6 +244,28 @@ export default {
})
: '' // disable the tooltip
},
/**
* Whether or not the information about the owner principal was loaded
*
* @returns {Boolean}
*/
loadedOwnerPrincipal() {
return this.getPrincipalByUrl(this.calendar.owner) !== undefined
},
ownerUserId() {
const principal = this.getPrincipalByUrl(this.calendar.owner)
if (principal) {
return principal.userId
}
return ''
},
ownerDisplayname() {
const principal = this.getPrincipalByUrl(this.calendar.owner)
if (principal) {
return principal.displayname
}
return ''
},
},
methods: {
...mapActions([

View file

@ -22,7 +22,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
<template>
<div class="content-wrapper">
<div v-if="task"
:class="{'disabled': task.calendar.readOnly}"
:class="{'disabled': readOnly}"
class="flex-container">
<div :class="{'editing': edit=='summary'}" class="title">
<span class="detail-checkbox">
@ -30,10 +30,10 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
type="checkbox"
class="checkbox"
name="detailsToggleCompleted"
:class="{'disabled': task.calendar.readOnly}"
:class="{'disabled': readOnly}"
:checked="task.completed"
:aria-checked="task.completed"
:disabled="task.calendar.readOnly"
:disabled="readOnly"
:aria-label="$t('tasks', 'Task is completed')"
@click="toggleCompleted(task)">
<label :for="'detailsToggleCompleted_' + task.uid" />
@ -56,16 +56,16 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
</div>
<TaskStatusDisplay :task="task" />
<button class="reactive inline" @click="togglePinned(task)">
<span :class="[{'disabled': task.calendar.readOnly}, iconPinned]" class="icon" />
<span :class="[{'disabled': readOnly}, iconPinned]" class="icon" />
</button>
<button class="reactive inline" @click="toggleStarred(task)">
<span :class="[{'disabled': task.calendar.readOnly}, iconStar]"
<span :class="[{'disabled': readOnly}, iconStar]"
class="icon" />
</button>
</div>
<div class="body">
<ul class="sections">
<li v-show="!task.calendar.readOnly || task.start"
<li v-show="!readOnly || task.start"
:class="{'date': task.startMoment.isValid(), 'editing': edit=='start', 'high': overdue(task.startMoment)}"
class="section detail-start">
<div v-click-outside="() => finishEditing('start')"
@ -109,7 +109,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
</button>
</div>
</li>
<li v-show="!task.calendar.readOnly || task.due"
<li v-show="!readOnly || task.due"
:class="{'date': task.dueMoment.isValid(), 'editing': edit=='due', 'high': overdue(task.dueMoment)}"
class="section detail-date">
<div v-click-outside="() => finishEditing('due')"
@ -160,10 +160,10 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
type="checkbox"
class="checkbox"
name="isAllDayPossible"
:class="{'disabled': task.calendar.readOnly}"
:class="{'disabled': readOnly}"
:aria-checked="allDay"
:checked="allDay"
:disabled="task.calendar.readOnly"
:disabled="readOnly"
@click="toggleAllDay(task)">
<label for="isAllDayPossible">
<span>{{ $t('tasks', 'All day') }}</span>
@ -182,7 +182,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
:value="task.calendar"
:multiple="false"
:allow-empty="false"
:disabled="task.calendar.readOnly"
:disabled="readOnly"
track-by="id"
:placeholder="$t('tasks', 'Select a calendar')"
label="displayName"
@ -206,7 +206,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
:value="classSelect.find( _ => _.type === task.class )"
:multiple="false"
:allow-empty="false"
:disabled="task.calendar.readOnly"
:disabled="readOnly || task.calendar.isSharedWithMe"
track-by="type"
:placeholder="$t('tasks', 'Select a classification')"
label="displayName"
@ -218,7 +218,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
</li>
<li v-show="!task.calendar.readOnly || task.status"
<li v-show="!readOnly || task.status"
class="section detail-class reactive">
<div v-click-outside="() => finishEditing('status')"
class="section-content"
@ -231,7 +231,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
:value="statusSelect.find( _ => _.type === task.status )"
:multiple="false"
:allow-empty="false"
:disabled="task.calendar.readOnly"
:disabled="readOnly"
track-by="type"
:placeholder="$t('tasks', 'Select a status')"
label="displayName"
@ -243,7 +243,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
</li>
<li v-show="!task.calendar.readOnly || task.priority"
<li v-show="!readOnly || task.priority"
:class="[{'editing': edit=='priority', 'date': task.priority>0}, priorityClass]"
class="section detail-priority">
<div v-click-outside="() => finishEditing('priority')"
@ -279,7 +279,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
</button>
</div>
</li>
<li v-show="!task.calendar.readOnly || task.complete"
<li v-show="!readOnly || task.complete"
:class="{'editing': edit=='complete', 'date': task.complete>0}"
class="section detail-complete">
<div v-click-outside="() => finishEditing('complete')"
@ -315,7 +315,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
</button>
</div>
</li>
<li v-show="!task.calendar.readOnly || task.categories.length>0" :class="{'active': task.categories.length>0}" class="section detail-categories">
<li v-show="!readOnly || task.categories.length>0" :class="{'active': task.categories.length>0}" class="section detail-categories">
<div class="section-content">
<span class="section-icon">
<span :class="[iconCategories]" class="icon detail-categories" />
@ -325,7 +325,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
v-model="task.categories"
:multiple="true"
:searchable="true"
:disabled="task.calendar.readOnly"
:disabled="readOnly"
:options="task.categories"
:placeholder="$t('tasks', 'Select categories')"
:taggable="true"
@ -337,7 +337,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
</li>
<li v-show="!task.calendar.readOnly || task.note" class="section detail-note">
<li v-show="!readOnly || task.note" class="section detail-note">
<div class="section-content note">
<div v-click-outside="() => finishEditing('note')"
class="note-body selectable"
@ -359,7 +359,7 @@ License along with this library. If not, see <http://www.gnu.org/licenses/>.
</ul>
</div>
<div class="footer">
<button :style="{visibility: task.calendar.readOnly ? 'hidden' : 'visible'}"
<button :style="{visibility: readOnly ? 'hidden' : 'visible'}"
class="close-all reactive inline"
@click="removeTask">
<span class="icon icon-sprt-bw sprt-trash" />
@ -446,6 +446,16 @@ export default {
}
},
computed: {
/**
* Whether we treat the task as read-only.
* We also treat tasks in shared calendars with an access class other than 'PUBLIC'
* as read-only.
*
* @returns {Boolean} Is the task read-only
*/
readOnly() {
return this.task.calendar.readOnly || (this.task.calendar.isSharedWithMe && this.task.class !== 'PUBLIC')
},
/**
* Whether the dates of a task are all-day
* When no dates are set, we consider the last used value.
@ -583,7 +593,7 @@ export default {
+ (this.task.completed ? ('<br />' + this.$t('tasks', 'Completed {date}', { date: this.task.completedDateMoment.calendar() })) : '')
},
isAllDayPossible: function() {
return !this.task.calendar.readOnly && (this.task.due || this.task.start || ['start', 'due'].includes(this.edit))
return !this.readOnly && (this.task.due || this.task.start || ['start', 'due'].includes(this.edit))
},
priorityClass: function() {
if (+this.task.priority > 5) {
@ -767,11 +777,15 @@ export default {
if (event && (event.target.classList.contains('mx-datepicker-btn-confirm') || event.target.tagName === 'A')) {
return
}
// Don't allow to change the access class in calendars shared with me.
if (this.task.calendar.isSharedWithMe && type === 'class') {
return
}
// Save possible edits before starting to edit another property.
if (this.edit !== type) {
this.finishEditing(this.edit)
}
if (!this.task.calendar.readOnly && this.edit !== type) {
if (!this.readOnly && this.edit !== type) {
this.edit = type
this.tmpTask[type] = this.task[type]
// If we edit the due or the start date, inintialize it.

67
src/models/principal.js Normal file
View file

@ -0,0 +1,67 @@
/**
* Nextcloud - Tasks
*
* @copyright Copyright (c) 2019 Georg Ehrke
*
* @author Georg Ehrke <oc.list@georgehrke.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
/**
* Creates a complete principal-object based on given props
*
* @param {Object} props Principal-props already provided
* @returns {any}
*/
export const getDefaultPrincipalObject = (props) => Object.assign({}, {
// Id of the principal
id: '',
// Calendar-user-type. This can be INDIVIDUAL, GROUP, RESOURCE or ROOM
calendarUserType: '',
// E-Mail address of principal used for scheduling
emailAddress: '',
// The principals display-name
displayname: '',
// principalScheme
principalScheme: '',
// The internal user-id in case it is of type INDIVIDUAL and a user
userId: '',
// url to the DAV-principal-resource
url: '',
// The cdav-library object
dav: false,
}, props)
/**
* converts a dav principal into a vuex object
*
* @param {Object} principal cdav-library Principal object
* @returns {{emailAddress: *, displayname: *, dav: *, id: *, calendarUserType: *, userId: *, url: *}}
*/
export function mapDavToPrincipal(principal) {
return {
id: btoa(principal.url),
calendarUserType: principal.calendarUserType,
principalScheme: principal.principalScheme,
emailAddress: principal.email,
displayname: principal.displayname,
userId: principal.userId,
url: principal.principalUrl,
dav: principal,
}
}

View file

@ -52,6 +52,10 @@ const calendarModel = {
dav: false,
supportsTasks: true,
loadedCompleted: false,
// Whether or not the calendar is shared with me
isSharedWithMe: false,
// Whether or not the calendar can be shared by me
canBeShared: false,
}
const state = {
@ -62,16 +66,26 @@ const state = {
* Maps a dav collection to our calendar object model
*
* @param {Object} calendar The calendar object from the cdav library
* @param {Object} currentUserPrincipal The principal model of the current user principal
* @returns {Object}
*/
export function mapDavCollectionToCalendar(calendar) {
export function mapDavCollectionToCalendar(calendar, currentUserPrincipal) {
const owner = calendar.owner
let isSharedWithMe = false
if (!currentUserPrincipal) {
// If the user is not authenticated, the calendar
// will always be marked as shared with them
isSharedWithMe = true
} else {
isSharedWithMe = (owner !== currentUserPrincipal.url)
}
return {
// get last part of url
id: calendar.url.split('/').slice(-2, -1)[0],
displayName: calendar.displayname,
color: calendar.color,
enabled: calendar.enabled !== false,
owner: calendar.owner,
owner,
readOnly: !calendar.isWriteable(),
tasks: {},
url: calendar.url,
@ -79,6 +93,8 @@ export function mapDavCollectionToCalendar(calendar) {
shares: calendar.shares.map(sharee => Object.assign({}, mapDavShareeToSharee(sharee))),
supportsTasks: calendar.components.includes('VTODO'),
loadedCompleted: false,
isSharedWithMe,
canBeShared: calendar.isShareable(),
}
}
@ -395,7 +411,7 @@ const actions = {
let calendars = await client.calendarHomes[0].findAllCalendars()
.then(calendars => {
return calendars.map(calendar => {
return mapDavCollectionToCalendar(calendar)
return mapDavCollectionToCalendar(calendar, context.getters.getCurrentUserPrincipal)
})
})
@ -419,7 +435,7 @@ const actions = {
async appendCalendar(context, calendar) {
return client.calendarHomes[0].createCalendarCollection(calendar.displayName, calendar.color, ['VTODO'])
.then((response) => {
calendar = mapDavCollectionToCalendar(response)
calendar = mapDavCollectionToCalendar(response, context.getters.getCurrentUserPrincipal)
context.commit('addCalendar', calendar)
// Open the calendar
router.push({ name: 'calendars', params: { calendarId: calendar.id } })

148
src/store/principals.js Normal file
View file

@ -0,0 +1,148 @@
/**
* Nextcloud - Tasks
*
* @author Georg Ehrke
* @copyright Copyright (c) 2019 Georg Ehrke <oc.list@georgehrke.com>
*
* @author Raimund Schlüßler
* @copyright 2020 Raimund Schlüßler <raimund.schluessler@mailbox.org>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import Vue from 'vue'
import client from '../services/cdav'
import { getDefaultPrincipalObject, mapDavToPrincipal } from '../models/principal'
const state = {
principals: [],
principalsById: {},
currentUserPrincipal: null,
}
const mutations = {
/**
* Adds a principal to the state
*
* @param {Object} state The vuex state
* @param {Object} data The destructuring object
* @param {Object} data.principal The principal to add
*/
addPrincipal(state, { principal }) {
const object = getDefaultPrincipalObject(principal)
if (state.principalsById[object.id]) {
return
}
state.principals.push(object)
Vue.set(state.principalsById, object.id, object)
},
/**
* Adds the current user principal to the state
*
* @param {Object} state The vuex state
* @param {Object} data destructuring object
* @param {String} data.principalId principalId of the current-user-principal
*/
setCurrentUserPrincipal(state, { principalId }) {
state.currentUserPrincipal = principalId
},
}
const getters = {
/**
* Gets a principal object by its url
*
* @param {Object} state the store data
* @returns {function({String}): {Object}}
*/
getPrincipalByUrl: (state) => (url) => state.principals.find((principal) => principal.url === url),
/**
* Gets a principal object by its id
*
* @param {Object} state the store data
* @returns {function({String}): {Object}}
*/
getPrincipalById: (state) => (id) => state.principalsById[id],
/**
* Gets the principal object of the current-user-principal
*
* @param {Object} state the store data
* @returns {{Object}}
*/
getCurrentUserPrincipal: (state) => state.principalsById[state.currentUserPrincipal],
/**
* Gets the email-address of the current-user-principal
*
* @param {Object} state the store data
* @returns {String}
*/
getCurrentUserPrincipalEmail: (state) => state.principalsById[state.currentUserPrincipal].emailAddress,
}
const actions = {
/**
* Fetches a principal from the DAV server and commits it to the state
*
* @param {Object} context The vuex context
* @param {String} url The URL of the principal
* @returns {Promise<void>}
*/
async fetchPrincipalByUrl(context, { url }) {
// Don't refetch principals we already have
if (context.getters.getPrincipalByUrl(url)) {
return
}
const principal = await client.findPrincipal(url)
if (!principal) {
// TODO - handle error
return
}
context.commit('addPrincipal', {
principal: mapDavToPrincipal(principal),
})
},
/**
* Fetches the current-user-principal
*
* @param {Object} context The vuex context
* @returns {Promise<void>}
*/
async fetchCurrentUserPrincipal(context) {
const currentUserPrincipal = client.currentUserPrincipal
if (!currentUserPrincipal) {
// TODO - handle error
return
}
const principal = mapDavToPrincipal(currentUserPrincipal)
context.commit('addPrincipal', { principal })
context.commit('setCurrentUserPrincipal', { principalId: principal.id })
},
}
export default { state, mutations, getters, actions }

View file

@ -26,6 +26,7 @@ import calendars from './calendars'
import collections from './collections'
import tasks from './tasks'
import settings from './settings'
import principals from './principals'
Vue.use(Vuex)
@ -35,5 +36,6 @@ export default new Vuex.Store({
collections,
tasks,
settings,
principals,
},
})

View file

@ -675,6 +675,10 @@ const actions = {
if (task.calendar.readOnly) {
return
}
// Don't delete tasks in shared calendars with access class not PUBLIC
if (task.calendar.isSharedWithMe && task.class !== 'PUBLIC') {
return
}
function deleteTaskFromStore() {
context.commit('deleteTask', task)
@ -728,6 +732,10 @@ const actions = {
if (task.calendar.readOnly) {
return
}
// Don't edit tasks in shared calendars with access class not PUBLIC
if (task.calendar.isSharedWithMe && task.class !== 'PUBLIC') {
return
}
const vCalendar = ICAL.stringify(task.jCal)
@ -847,6 +855,10 @@ const actions = {
if (task.calendar.readOnly) {
return
}
// Don't edit tasks in shared calendars with access class not PUBLIC
if (task.calendar.isSharedWithMe && task.class !== 'PUBLIC') {
return
}
if (task.completed) {
await context.dispatch('setPercentComplete', { task: task, complete: 0 })
} else {
@ -912,6 +924,10 @@ const actions = {
if (task.calendar.readOnly) {
return
}
// Don't edit tasks in shared calendars with access class not PUBLIC
if (task.calendar.isSharedWithMe && task.class !== 'PUBLIC') {
return
}
context.commit('toggleStarred', task)
context.dispatch('scheduleTaskUpdate', task)
},
@ -927,6 +943,10 @@ const actions = {
if (task.calendar.readOnly) {
return
}
// Don't edit tasks in shared calendars with access class not PUBLIC
if (task.calendar.isSharedWithMe && task.class !== 'PUBLIC') {
return
}
context.commit('togglePinned', task)
context.dispatch('scheduleTaskUpdate', task)
},
@ -1085,6 +1105,10 @@ const actions = {
if (task.calendar.readOnly) {
return task
}
// Don't edit tasks in shared calendars with access class not PUBLIC
if (task.calendar.isSharedWithMe && task.class !== 'PUBLIC') {
return
}
context.commit('toggleAllDay', task)
if (+context.rootState.settings.settings.allDay !== +task.allDay) {
context.dispatch('setSetting', { type: 'allDay', value: +task.allDay })
@ -1159,7 +1183,10 @@ const actions = {
if (task.calendar.readOnly) {
return task
}
// Don't move tasks in shared calendars with access class not PUBLIC
if (task.calendar.isSharedWithMe && task.class !== 'PUBLIC') {
return
}
// Don't move if source and target calendar are the same.
if (task.dav && task.calendar !== calendar) {
// Move all subtasks first

View file

@ -25,6 +25,7 @@ const calendarsData = [
displayname: 'Calendar 1',
color: '#123456',
isWriteable: () => { return true },
isShareable: () => { return true },
shares: [],
components: ['VTODO'],
calendarQuery: () => { return null },
@ -177,6 +178,7 @@ END:VCALENDAR`],
displayname: 'Calendar 2',
color: '#123456',
isWriteable: () => { return true },
isShareable: () => { return true },
shares: [],
components: ['VTODO', 'VEVENT'],
calendarQuery: () => { return null },