Merge pull request #862 from nextcloud/fix/467
Prevent editing not public tasks in lists shared with me
This commit is contained in:
commit
f58e8bd12f
15 changed files with 472 additions and 212 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,11 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.app-navigation-entry__utils .icon-loading {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&.edit {
|
||||
|
|
|
@ -27,4 +27,3 @@
|
|||
|
||||
@import './src/Calendars/Calendar.scss';
|
||||
@import './src/Calendars/CalendarShares.scss';
|
||||
@import './src/Calendars/CalendarSharee.scss';
|
||||
|
|
45
src/App.vue
45
src/App.vue
|
@ -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: {
|
||||
/**
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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
67
src/models/principal.js
Normal 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,
|
||||
}
|
||||
}
|
|
@ -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
148
src/store/principals.js
Normal 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 }
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 },
|
||||
|
|
Loading…
Reference in a new issue