Hide UI elements if the user does not have the right to use them (#211)

Hide Team UI elements if the user does not have the rights to use them

Fix replacing the right saved in the model when updating

Hide UI-Elements on task if the user does not have the rights to use them

Hide UI-Elements on gantt if the user does not have the rights to use them

Hide UI-Elements on kanban if the user does not have rights to use them

Fix canWrite condition

Hide list components if the user has no right to write to the list

Add max right to model

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/211
This commit is contained in:
konrad 2020-08-11 18:18:59 +00:00
parent e64b4e3329
commit 3c07c6e8c0
22 changed files with 282 additions and 132 deletions

View file

@ -34,7 +34,10 @@
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"> :style="{ 'opacity': currentList.title === '' ? '0': '1' }">
{{ currentList.title === '' ? 'Loading...': currentList.title}} {{ currentList.title === '' ? 'Loading...': currentList.title}}
</h1> </h1>
<router-link :to="{ name: 'list.edit', params: { id: currentList.id } }" class="icon"> <router-link
:to="{ name: 'list.edit', params: { id: currentList.id } }"
class="icon"
v-if="canWriteCurrentList">
<icon icon="cog" size="2x"/> <icon icon="cog" size="2x"/>
</router-link> </router-link>
</div> </div>
@ -291,6 +294,7 @@
import NamespaceService from './services/namespace' import NamespaceService from './services/namespace'
import authTypes from './models/authTypes' import authTypes from './models/authTypes'
import Rights from './models/rights.json'
import swEvents from './ServiceWorker/events' import swEvents from './ServiceWorker/events'
import Notification from './components/misc/notification' import Notification from './components/misc/notification'
@ -427,6 +431,7 @@
background: 'background', background: 'background',
imprintUrl: state => state.config.legal.imprintUrl, imprintUrl: state => state.config.legal.imprintUrl,
privacyPolicyUrl: state => state.config.legal.privacyPolicyUrl, privacyPolicyUrl: state => state.config.legal.privacyPolicyUrl,
canWriteCurrentList: state => state.currentList.maxRight > Rights.READ,
}), }),
methods: { methods: {
logout() { logout() {

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="editor"> <div class="editor" :class="{'is-pulled-up': isEditEnabled}">
<div class="tabs is-right" v-if="hasPreview"> <div class="tabs is-right" v-if="hasPreview && isEditEnabled">
<ul> <ul>
<li :class="{'is-active': isPreviewActive}" v-if="isEditActive"> <li :class="{'is-active': isPreviewActive}" v-if="isEditActive">
<a @click="showPreview">Preview</a> <a @click="showPreview">Preview</a>
@ -58,6 +58,9 @@
type: Boolean, type: Boolean,
default: true, default: true,
}, },
isEditEnabled: {
default: true,
},
}, },
data() { data() {
return { return {

View file

@ -35,7 +35,7 @@
'has-dark-text': colorIsDark(t.hexColor) 'has-dark-text': colorIsDark(t.hexColor)
}" }"
:style="{'border-color': t.hexColor, 'background-color': t.hexColor}" :style="{'border-color': t.hexColor, 'background-color': t.hexColor}"
:isActive="true" :isActive="canWrite"
:x="t.offsetDays * dayWidth - 6" :x="t.offsetDays * dayWidth - 6"
:y="0" :y="0"
:w="t.durationDays * dayWidth" :w="t.durationDays * dayWidth"
@ -67,7 +67,7 @@
<div class="row" v-for="(t, k) in tasksWithoutDates" :key="t.id" :style="{background: 'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' + (k % 2 === 0 ? '#fafafa 1px, #fafafa ' : '#fff 1px, #fff ') + dayWidth + 'px)'}"> <div class="row" v-for="(t, k) in tasksWithoutDates" :key="t.id" :style="{background: 'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' + (k % 2 === 0 ? '#fafafa 1px, #fafafa ' : '#fff 1px, #fff ') + dayWidth + 'px)'}">
<VueDragResize <VueDragResize
class="task nodate" class="task nodate"
:isActive="true" :isActive="canWrite"
:x="dayOffsetUntilToday * dayWidth - 6" :x="dayOffsetUntilToday * dayWidth - 6"
:y="0" :y="0"
:h="31" :h="31"
@ -88,7 +88,7 @@
</div> </div>
</template> </template>
</div> </div>
<form @submit.prevent="addNewTask()" class="add-new-task"> <form @submit.prevent="addNewTask()" class="add-new-task" v-if="canWrite">
<transition name="width"> <transition name="width">
<input <input
type="text" type="text"
@ -138,6 +138,8 @@
import priorities from '../../models/priorities' import priorities from '../../models/priorities'
import PriorityLabel from './partials/priorityLabel' import PriorityLabel from './partials/priorityLabel'
import TaskCollectionService from '../../services/taskCollection' import TaskCollectionService from '../../services/taskCollection'
import {mapState} from 'vuex'
import Rights from '../../models/rights.json'
export default { export default {
name: 'GanttChart', name: 'GanttChart',
@ -201,6 +203,9 @@
mounted() { mounted() {
this.buildTheGanttChart() this.buildTheGanttChart()
}, },
computed: mapState({
canWrite: state => state.currentList.maxRight > Rights.READ,
}),
methods: { methods: {
buildTheGanttChart() { buildTheGanttChart() {
this.setDates() this.setDates()

View file

@ -6,6 +6,7 @@
</span> </span>
Attachments Attachments
<a <a
v-if="editEnabled"
class="button is-primary is-outlined is-small noshadow" class="button is-primary is-outlined is-small noshadow"
@click="$refs.files.click()" @click="$refs.files.click()"
:disabled="attachmentService.loading"> :disabled="attachmentService.loading">
@ -14,7 +15,7 @@
</a> </a>
</h3> </h3>
<input type="file" id="files" ref="files" multiple @change="uploadNewAttachment()" :disabled="attachmentService.loading"/> <input type="file" id="files" ref="files" multiple @change="uploadNewAttachment()" :disabled="attachmentService.loading" v-if="editEnabled"/>
<progress v-if="attachmentService.uploadProgress > 0" class="progress is-primary" :value="attachmentService.uploadProgress" max="100">{{ attachmentService.uploadProgress }}%</progress> <progress v-if="attachmentService.uploadProgress > 0" class="progress is-primary" :value="attachmentService.uploadProgress" max="100">{{ attachmentService.uploadProgress }}%</progress>
<table> <table>
@ -41,7 +42,7 @@
<icon icon="cloud-download-alt"/> <icon icon="cloud-download-alt"/>
</span> </span>
</a> </a>
<a class="button is-danger noshadow" v-tooltip="'Delete this attachment'" @click="() => {attachmentToDelete = a; showDeleteModal = true}"> <a v-if="editEnabled" class="button is-danger noshadow" v-tooltip="'Delete this attachment'" @click="() => {attachmentToDelete = a; showDeleteModal = true}">
<span class="icon"> <span class="icon">
<icon icon="trash-alt"/> <icon icon="trash-alt"/>
</span> </span>
@ -52,7 +53,7 @@
</table> </table>
<!-- Dropzone --> <!-- Dropzone -->
<div class="dropzone" :class="{ 'hidden': !showDropzone }"> <div class="dropzone" :class="{ 'hidden': !showDropzone }" v-if="editEnabled">
<div class="drop-hint"> <div class="drop-hint">
<div class="icon"> <div class="icon">
<icon icon="cloud-upload-alt"/> <icon icon="cloud-upload-alt"/>
@ -102,7 +103,10 @@
}, },
initialAttachments: { initialAttachments: {
type: Array, type: Array,
} },
editEnabled: {
default: true,
},
}, },
created() { created() {
this.attachmentService = new AttachmentService() this.attachmentService = new AttachmentService()

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="content details has-top-border"> <div class="content details" :class="{'has-top-border': canWrite || comments.length > 0}">
<h1> <h1 v-if="canWrite || comments.length > 0">
<span class="icon is-grey"> <span class="icon is-grey">
<icon :icon="['far', 'comments']"/> <icon :icon="['far', 'comments']"/>
</span> </span>
@ -15,7 +15,7 @@
<img class="image is-avatar" :src="c.author.getAvatarUrl(48)" alt="" width="48" height="48"/> <img class="image is-avatar" :src="c.author.getAvatarUrl(48)" alt="" width="48" height="48"/>
</figure> </figure>
<div class="media-content"> <div class="media-content">
<div class="comment-info"> <div class="comment-info" :class="{'is-pulled-up': canWrite}">
<strong>{{ c.author.username }}</strong>&nbsp; <strong>{{ c.author.username }}</strong>&nbsp;
<small v-tooltip="formatDate(c.created)">{{ formatDateSince(c.created) }}</small> <small v-tooltip="formatDate(c.created)">{{ formatDateSince(c.created) }}</small>
<small v-if="+new Date(c.created) !== +new Date(c.updated)" v-tooltip="formatDate(c.updated)"> · <small v-if="+new Date(c.created) !== +new Date(c.updated)" v-tooltip="formatDate(c.updated)"> ·
@ -27,13 +27,14 @@
@change="() => {toggleEdit(c);editComment()}" @change="() => {toggleEdit(c);editComment()}"
:upload-enabled="true" :upload-enabled="true"
:upload-callback="attachmentUpload" :upload-callback="attachmentUpload"
:is-edit-enabled="canWrite"
/> />
<div class="comment-actions"> <div class="comment-actions" v-if="canWrite">
<a @click="toggleDelete(c.id)">Remove</a> <a @click="toggleDelete(c.id)">Remove</a>
</div> </div>
</div> </div>
</div> </div>
<div class="media comment"> <div class="media comment" v-if="canWrite">
<figure class="media-left"> <figure class="media-left">
<img class="image is-avatar" :src="userAvatar" alt="" width="48" height="48"/> <img class="image is-avatar" :src="userAvatar" alt="" width="48" height="48"/>
</figure> </figure>
@ -95,7 +96,10 @@
taskId: { taskId: {
type: Number, type: Number,
required: true, required: true,
} },
canWrite: {
default: true,
},
}, },
data() { data() {
return { return {
@ -125,7 +129,7 @@
watch: { watch: {
taskId() { taskId() {
this.loadComments() this.loadComments()
} },
}, },
computed: { computed: {
userAvatar() { userAvatar() {

View file

@ -17,10 +17,11 @@
track-by="id" track-by="id"
select-label="Assign this user" select-label="Assign this user"
:showNoOptions="false" :showNoOptions="false"
:disabled="disabled"
> >
<template slot="tag" slot-scope="{ option }"> <template slot="tag" slot-scope="{ option }">
<user :user="option" :show-username="false" :avatar-size="30"/> <user :user="option" :show-username="false" :avatar-size="30"/>
<a @click="removeAssignee(option)" class="remove-assignee"> <a @click="removeAssignee(option)" class="remove-assignee" v-if="!disabled">
<icon icon="times"/> <icon icon="times"/>
</a> </a>
</template> </template>
@ -65,7 +66,10 @@
initialAssignees: { initialAssignees: {
type: Array, type: Array,
default: () => [], default: () => [],
} },
disabled: {
default: false,
},
}, },
data() { data() {
return { return {

View file

@ -19,6 +19,7 @@
:showNoOptions="false" :showNoOptions="false"
@tag="createAndAddLabel" @tag="createAndAddLabel"
tag-placeholder="Add this as new label" tag-placeholder="Add this as new label"
:disabled="disabled"
> >
<template slot="tag" slot-scope="{ option }"> <template slot="tag" slot-scope="{ option }">
<span class="tag" <span class="tag"
@ -54,6 +55,9 @@
type: Number, type: Number,
required: true, required: true,
}, },
disabled: {
default: false,
},
}, },
data() { data() {
return { return {

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="select"> <div class="select">
<select v-model.number="percentDone" @change="updateData"> <select v-model.number="percentDone" @change="updateData" :disabled="disabled">
<option value="0">0%</option> <option value="0">0%</option>
<option value="0.1">10%</option> <option value="0.1">10%</option>
<option value="0.2">20%</option> <option value="0.2">20%</option>
@ -28,7 +28,10 @@
value: { value: {
default: 0, default: 0,
type: Number, type: Number,
} },
disabled: {
default: false,
},
}, },
watch: { watch: {
// Set the priority to the :value every time it changes from the outside // Set the priority to the :value every time it changes from the outside

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="select"> <div class="select">
<select v-model="priority" @change="updateData"> <select v-model="priority" @change="updateData" :disabled="disabled">
<option :value="priorities.UNSET">Unset</option> <option :value="priorities.UNSET">Unset</option>
<option :value="priorities.LOW">Low</option> <option :value="priorities.LOW">Low</option>
<option :value="priorities.MEDIUM">Medium</option> <option :value="priorities.MEDIUM">Medium</option>
@ -26,7 +26,10 @@
value: { value: {
default: 0, default: 0,
type: Number, type: Number,
} },
disabled: {
default: false,
},
}, },
watch: { watch: {
// Set the priority to the :value every time it changes from the outside // Set the priority to the :value every time it changes from the outside

View file

@ -1,5 +1,6 @@
<template> <template>
<div class="task-relations"> <div class="task-relations">
<template v-if="editEnabled">
<label class="label">New Task Relation</label> <label class="label">New Task Relation</label>
<div class="field"> <div class="field">
<multiselect <multiselect
@ -42,6 +43,7 @@
<a class="button is-primary" @click="addTaskRelation()">Add task Relation</a> <a class="button is-primary" @click="addTaskRelation()">Add task Relation</a>
</div> </div>
</div> </div>
</template>
<div class="related-tasks" v-for="(rts, kind ) in relatedTasks" :key="kind"> <div class="related-tasks" v-for="(rts, kind ) in relatedTasks" :key="kind">
<template v-if="rts.length > 0"> <template v-if="rts.length > 0">
@ -50,13 +52,17 @@
<div class="task" v-for="t in rts" :key="t.id"> <div class="task" v-for="t in rts" :key="t.id">
<router-link :to="{ name: $route.name, params: { id: t.id } }"> <router-link :to="{ name: $route.name, params: { id: t.id } }">
<span class="tasktext" :class="{ 'done': t.done}"> <span class="tasktext" :class="{ 'done': t.done}">
<span v-if="t.listId !== listId" class="different-list" v-tooltip="'This task belongs to a different list.'"> <span
v-if="t.listId !== listId"
class="different-list"
v-tooltip="'This task belongs to a different list.'">
{{ $store.getters['lists/getListById'](t.listId) === null ? '' : $store.getters['lists/getListById'](t.listId).title }} > {{ $store.getters['lists/getListById'](t.listId) === null ? '' : $store.getters['lists/getListById'](t.listId).title }} >
</span> </span>
{{t.title}} {{t.title}}
</span> </span>
</router-link> </router-link>
<a <a
v-if="editEnabled"
class="remove" class="remove"
@click="() => {showDeleteModal = true; relationToDelete = {relationKind: kind, otherTaskId: t.id}}"> @click="() => {showDeleteModal = true; relationToDelete = {relationKind: kind, otherTaskId: t.id}}">
<icon icon="trash-alt"/> <icon icon="trash-alt"/>
@ -131,7 +137,10 @@
listId: { listId: {
type: Number, type: Number,
default: 0, default: 0,
} },
editEnabled: {
default: true,
},
}, },
created() { created() {
this.taskService = new TaskService() this.taskService = new TaskService()
@ -222,7 +231,7 @@
return relationKinds[kind][1] return relationKinds[kind][1]
} }
return relationKinds[kind][0] return relationKinds[kind][0]
} },
}, },
} }
</script> </script>

View file

@ -10,9 +10,10 @@
:value="r" :value="r"
:data-index="index" :data-index="index"
placeholder="Add a new reminder..." placeholder="Add a new reminder..."
:disabled="disabled"
> >
</flat-pickr> </flat-pickr>
<a v-if="index !== (reminders.length - 1)" @click="removeReminderByIndex(index)"> <a v-if="index !== (reminders.length - 1) && !disabled" @click="removeReminderByIndex(index)">
<icon icon="times"></icon> <icon icon="times"></icon>
</a> </a>
</div> </div>
@ -44,7 +45,10 @@
value: { value: {
default: () => [], default: () => [],
type: Array, type: Array,
} },
disabled: {
default: false,
},
}, },
components: { components: {
flatPickr, flatPickr,

View file

@ -6,6 +6,7 @@
<div class="column is-7 field has-addons"> <div class="column is-7 field has-addons">
<div class="control"> <div class="control">
<input <input
:disabled="disabled"
class="input" class="input"
placeholder="Specify an amount..." placeholder="Specify an amount..."
v-model="repeatAfter.amount" v-model="repeatAfter.amount"
@ -13,7 +14,7 @@
</div> </div>
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select v-model="repeatAfter.type" @change="updateData"> <select v-model="repeatAfter.type" @change="updateData" :disabled="disabled">
<option value="hours">Hours</option> <option value="hours">Hours</option>
<option value="days">Days</option> <option value="days">Days</option>
<option value="weeks">Weeks</option> <option value="weeks">Weeks</option>
@ -24,6 +25,7 @@
</div> </div>
</div> </div>
<fancycheckbox <fancycheckbox
:disabled="disabled"
class="column" class="column"
@change="updateData" @change="updateData"
v-model="task.repeatFromCurrentDate" v-model="task.repeatFromCurrentDate"
@ -54,7 +56,10 @@
default: () => { default: () => {
}, },
required: true, required: true,
} },
disabled: {
default: false,
},
}, },
watch: { watch: {
value(newVal) { value(newVal) {

View file

@ -1,6 +1,6 @@
<template> <template>
<span> <span>
<fancycheckbox v-model="task.done" @change="markAsDone" :disabled="isArchived"/> <fancycheckbox v-model="task.done" @change="markAsDone" :disabled="isArchived || disabled"/>
<span class="tasktext" :class="{ 'done': task.done}"> <span class="tasktext" :class="{ 'done': task.done}">
<router-link :to="{ name: taskDetailRoute, params: { id: task.id } }"> <router-link :to="{ name: taskDetailRoute, params: { id: task.id } }">
<router-link <router-link
@ -88,6 +88,10 @@
type: Boolean, type: Boolean,
default: false, default: false,
}, },
disabled: {
type: Boolean,
default: false,
},
}, },
watch: { watch: {
theTask(newVal) { theTask(newVal) {

View file

@ -3,6 +3,12 @@ import {objectToCamelCase} from '../helpers/case'
export default class AbstractModel { export default class AbstractModel {
/**
* The max right the user has on this object, as returned by the x-max-right header from the api.
* @type {number|null}
*/
maxRight = null
/** /**
* The abstract constructor takes an object and merges its data with the default data of this model. * The abstract constructor takes an object and merges its data with the default data of this model.
* @param data * @param data

View file

@ -310,7 +310,9 @@ export default class AbstractService {
return this.errorHandler(error) return this.errorHandler(error)
}) })
.then(response => { .then(response => {
return Promise.resolve(this.modelGetFactory(response.data)) const result = this.modelGetFactory(response.data)
result.maxRight = Number(response.headers['x-max-right'])
return Promise.resolve(result)
}) })
.finally(() => { .finally(() => {
cancel() cancel()
@ -377,7 +379,11 @@ export default class AbstractService {
return this.errorHandler(error) return this.errorHandler(error)
}) })
.then(response => { .then(response => {
return Promise.resolve(this.modelCreateFactory(response.data)) const result = this.modelCreateFactory(response.data)
if(typeof model.maxRight !== 'undefined') {
result.maxRight = model.maxRight
}
return Promise.resolve(result)
}) })
.finally(() => { .finally(() => {
cancel() cancel()
@ -399,7 +405,11 @@ export default class AbstractService {
return this.errorHandler(error) return this.errorHandler(error)
}) })
.then(response => { .then(response => {
return Promise.resolve(this.modelUpdateFactory(response.data)) const result = this.modelUpdateFactory(response.data)
if(typeof model.maxRight !== 'undefined') {
result.maxRight = model.maxRight
}
return Promise.resolve(result)
}) })
.finally(() => { .finally(() => {
cancel() cancel()

View file

@ -347,7 +347,6 @@ fieldset[disabled] .multiselect {
} }
.multiselect--disabled { .multiselect--disabled {
background: $multiselect-disabled;
pointer-events: none; pointer-events: none;
.multiselect__current, .multiselect__select { .multiselect__current, .multiselect__select {
background: $multiselect-disabled; background: $multiselect-disabled;

View file

@ -6,7 +6,7 @@
} }
@media screen and (min-width: $tablet) { @media screen and (min-width: $tablet) {
.comment-info { .comment-info.is-pulled-up {
margin-bottom: -3rem; margin-bottom: -3rem;
} }
} }

View file

@ -112,7 +112,10 @@
} }
&.description .editor { &.description .editor {
&.is-pulled-up {
margin-top: -3.5rem; margin-top: -3.5rem;
}
.tabs { .tabs {
margin-bottom: 0; margin-bottom: 0;
@ -139,6 +142,7 @@
font-style: italic; font-style: italic;
} }
&:not(:disabled) {
&:hover, &:active { &:hover, &:active {
background: $input-background-color; background: $input-background-color;
border-color: $input-border-color; border-color: $input-border-color;
@ -150,6 +154,7 @@
border-color: $input-focus-border-color; border-color: $input-focus-border-color;
} }
} }
}
.select:not(.has-defaults):after { .select:not(.has-defaults):after {
opacity: 0; opacity: 0;

View file

@ -9,7 +9,11 @@
@focusout="() => saveBucketTitle(bucket.id)" @focusout="() => saveBucketTitle(bucket.id)"
:ref="`bucket${bucket.id}title`" :ref="`bucket${bucket.id}title`"
@keyup.ctrl.enter="() => saveBucketTitle(bucket.id)">{{ bucket.title }}</h2> @keyup.ctrl.enter="() => saveBucketTitle(bucket.id)">{{ bucket.title }}</h2>
<div class="dropdown is-right options" :class="{ 'is-active': bucketOptionsDropDownActive[bucket.id] }"> <div
class="dropdown is-right options"
:class="{ 'is-active': bucketOptionsDropDownActive[bucket.id] }"
v-if="canWrite"
>
<div class="dropdown-trigger" @click.stop="toggleBucketDropdown(bucket.id)"> <div class="dropdown-trigger" @click.stop="toggleBucketDropdown(bucket.id)">
<span class="icon"> <span class="icon">
<icon icon="ellipsis-v"/> <icon icon="ellipsis-v"/>
@ -31,7 +35,9 @@
</div> </div>
</div> </div>
<div class="tasks" :ref="`tasks-container${bucket.id}`"> <div class="tasks" :ref="`tasks-container${bucket.id}`">
<Container <!-- Make the component either a div or a draggable component based on the user rights -->
<component
:is="canWrite ? 'Container' : 'div'"
@drop="e => onDrop(bucket.id, e)" @drop="e => onDrop(bucket.id, e)"
group-name="buckets" group-name="buckets"
:get-child-payload="getTaskPayload(bucket.id)" :get-child-payload="getTaskPayload(bucket.id)"
@ -41,7 +47,12 @@
drag-class-drop="ghost-task-drop" drag-class-drop="ghost-task-drop"
drag-handle-selector=".task.draggable" drag-handle-selector=".task.draggable"
> >
<Draggable v-for="task in bucket.tasks" :key="`bucket${bucket.id}-task${task.id}`"> <!-- Make the component either a div or a draggable component based on the user rights -->
<component
v-for="task in bucket.tasks"
:key="`bucket${bucket.id}-task${task.id}`"
:is="canWrite ? 'Draggable' : 'div'"
>
<div <div
class="task loader-container draggable" class="task loader-container draggable"
:class="{ :class="{
@ -103,10 +114,10 @@
</div> </div>
</div> </div>
</div> </div>
</Draggable> </component>
</Container> </component>
</div> </div>
<div class="bucket-footer"> <div class="bucket-footer" v-if="canWrite">
<div class="field" v-if="showNewTaskInput[bucket.id]"> <div class="field" v-if="showNewTaskInput[bucket.id]">
<div class="control"> <div class="control">
<input <input
@ -144,7 +155,7 @@
</div> </div>
</div> </div>
<div class="bucket new-bucket" v-if="!loading"> <div class="bucket new-bucket" v-if="!loading && canWrite">
<input <input
v-if="showNewBucketInput" v-if="showNewBucketInput"
class="input" class="input"
@ -204,6 +215,7 @@
import {mapState} from 'vuex' import {mapState} from 'vuex'
import {LOADING} from '../../../store/mutation-types' import {LOADING} from '../../../store/mutation-types'
import {saveListView} from '../../../helpers/saveListView' import {saveListView} from '../../../helpers/saveListView'
import Rights from '../../../models/rights.json'
export default { export default {
name: 'Kanban', name: 'Kanban',
@ -254,6 +266,7 @@
buckets: state => state.kanban.buckets, buckets: state => state.kanban.buckets,
loadedListId: state => state.kanban.listId, loadedListId: state => state.kanban.listId,
loading: LOADING, loading: LOADING,
canWrite: state => state.currentList.maxRight > Rights.READ,
}), }),
methods: { methods: {
loadBuckets() { loadBuckets() {
@ -385,7 +398,7 @@
const task = new TaskModel({ const task = new TaskModel({
title: this.newTaskText, title: this.newTaskText,
bucketId: this.buckets[bi].id, bucketId: this.buckets[bi].id,
listId: this.$route.params.listId listId: this.$route.params.listId,
}) })
this.taskService.create(task) this.taskService.create(task)
@ -410,7 +423,7 @@
const newBucket = new BucketModel({ const newBucket = new BucketModel({
title: this.newBucketTitle, title: this.newBucketTitle,
listId: parseInt(this.$route.params.listId) listId: parseInt(this.$route.params.listId),
}) })
this.$store.dispatch('kanban/createBucket', newBucket) this.$store.dispatch('kanban/createBucket', newBucket)

View file

@ -48,7 +48,7 @@
</transition> </transition>
</div> </div>
<div class="field task-add" v-if="!list.isArchived"> <div class="field task-add" v-if="!list.isArchived && canWrite">
<div class="field is-grouped"> <div class="field is-grouped">
<p class="control has-icons-left is-expanded" :class="{ 'is-loading': taskService.loading}"> <p class="control has-icons-left is-expanded" :class="{ 'is-loading': taskService.loading}">
<input <input
@ -85,8 +85,13 @@
<div class="column"> <div class="column">
<div class="tasks" v-if="tasks && tasks.length > 0" :class="{'short': isTaskEdit}"> <div class="tasks" v-if="tasks && tasks.length > 0" :class="{'short': isTaskEdit}">
<div class="task" v-for="t in tasks" :key="t.id"> <div class="task" v-for="t in tasks" :key="t.id">
<single-task-in-list :the-task="t" @taskUpdated="updateTasks" task-detail-route="task.detail"/> <single-task-in-list
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived"> :the-task="t"
@taskUpdated="updateTasks"
task-detail-route="task.detail"
:disabled="!canWrite"
/>
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived && canWrite">
<icon icon="pencil-alt"/> <icon icon="pencil-alt"/>
</div> </div>
</div> </div>
@ -169,6 +174,8 @@
import taskList from '../../../components/tasks/mixins/taskList' import taskList from '../../../components/tasks/mixins/taskList'
import {saveListView} from '../../../helpers/saveListView' import {saveListView} from '../../../helpers/saveListView'
import Filters from '../../../components/list/partials/filters' import Filters from '../../../components/list/partials/filters'
import Rights from '../../../models/rights.json'
import {mapState} from 'vuex'
export default { export default {
name: 'List', name: 'List',
@ -202,6 +209,9 @@
// We use local storage and not vuex here to make it persistent across reloads. // We use local storage and not vuex here to make it persistent across reloads.
saveListView(this.$route.params.listId, this.$route.name) saveListView(this.$route.params.listId, this.$route.name)
}, },
computed: mapState({
canWrite: state => state.currentList.maxRight > Rights.READ,
}),
methods: { methods: {
// This function initializes the tasks page and loads the first page of tasks // This function initializes the tasks page and loads the first page of tasks
initTasks(page, search = '') { initTasks(page, search = '') {

View file

@ -22,7 +22,7 @@
<!-- Content and buttons --> <!-- Content and buttons -->
<div class="columns"> <div class="columns">
<!-- Content --> <!-- Content -->
<div class="column is-two-thirds"> <div class="column" :class="{'is-two-thirds': canWrite}">
<div class="columns details"> <div class="columns details">
<div class="column assignees" v-if="activeFields.assignees"> <div class="column assignees" v-if="activeFields.assignees">
<!-- Assignees --> <!-- Assignees -->
@ -35,6 +35,7 @@
:list-id="task.listId" :list-id="task.listId"
:initial-assignees="task.assignees" :initial-assignees="task.assignees"
ref="assignees" ref="assignees"
:disabled="!canWrite"
/> />
</div> </div>
<div class="column" v-if="activeFields.priority"> <div class="column" v-if="activeFields.priority">
@ -43,7 +44,11 @@
<icon :icon="['far', 'star']"/> <icon :icon="['far', 'star']"/>
Priority Priority
</div> </div>
<priority-select v-model="task.priority" @change="saveTask" ref="priority"/> <priority-select
v-model="task.priority"
@change="saveTask"
ref="priority"
:disabled="!canWrite"/>
</div> </div>
<div class="column" v-if="activeFields.dueDate"> <div class="column" v-if="activeFields.dueDate">
<!-- Due Date --> <!-- Due Date -->
@ -55,7 +60,7 @@
<flat-pickr <flat-pickr
:class="{ 'disabled': taskService.loading}" :class="{ 'disabled': taskService.loading}"
class="input" class="input"
:disabled="taskService.loading" :disabled="taskService.loading || !canWrite"
v-model="dueDate" v-model="dueDate"
:config="flatPickerConfig" :config="flatPickerConfig"
@on-close="saveTask" @on-close="saveTask"
@ -63,7 +68,7 @@
ref="dueDate" ref="dueDate"
> >
</flat-pickr> </flat-pickr>
<a v-if="dueDate" @click="() => {dueDate = task.dueDate = null;saveTask()}"> <a v-if="dueDate && canWrite" @click="() => {dueDate = task.dueDate = null;saveTask()}">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="times"></icon> <icon icon="times"></icon>
</span> </span>
@ -76,7 +81,11 @@
<icon icon="percent"/> <icon icon="percent"/>
Percent Done Percent Done
</div> </div>
<percent-done-select v-model="task.percentDone" @change="saveTask" ref="percentDone"/> <percent-done-select
v-model="task.percentDone"
@change="saveTask"
ref="percentDone"
:disabled="!canWrite"/>
</div> </div>
<div class="column" v-if="activeFields.startDate"> <div class="column" v-if="activeFields.startDate">
<!-- Start Date --> <!-- Start Date -->
@ -88,7 +97,7 @@
<flat-pickr <flat-pickr
:class="{ 'disabled': taskService.loading}" :class="{ 'disabled': taskService.loading}"
class="input" class="input"
:disabled="taskService.loading" :disabled="taskService.loading || !canWrite"
v-model="task.startDate" v-model="task.startDate"
:config="flatPickerConfig" :config="flatPickerConfig"
@on-close="saveTask" @on-close="saveTask"
@ -96,7 +105,7 @@
ref="startDate" ref="startDate"
> >
</flat-pickr> </flat-pickr>
<a v-if="task.startDate" @click="() => {task.startDate = null;saveTask()}"> <a v-if="task.startDate && canWrite" @click="() => {task.startDate = null;saveTask()}">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="times"></icon> <icon icon="times"></icon>
</span> </span>
@ -113,7 +122,7 @@
<flat-pickr <flat-pickr
:class="{ 'disabled': taskService.loading}" :class="{ 'disabled': taskService.loading}"
class="input" class="input"
:disabled="taskService.loading" :disabled="taskService.loading || !canWrite"
v-model="task.endDate" v-model="task.endDate"
:config="flatPickerConfig" :config="flatPickerConfig"
@on-close="saveTask" @on-close="saveTask"
@ -121,7 +130,7 @@
ref="endDate" ref="endDate"
> >
</flat-pickr> </flat-pickr>
<a v-if="task.endDate" @click="() => {task.endDate = null;saveTask()}"> <a v-if="task.endDate && canWrite" @click="() => {task.endDate = null;saveTask()}">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="times"></icon> <icon icon="times"></icon>
</span> </span>
@ -134,7 +143,11 @@
<icon icon="history"/> <icon icon="history"/>
Reminders Reminders
</div> </div>
<reminders v-model="task.reminderDates" @change="saveTask" ref="reminders"/> <reminders
v-model="task.reminderDates"
@change="saveTask"
ref="reminders"
:disabled="!canWrite"/>
</div> </div>
<div class="column" v-if="activeFields.repeatAfter"> <div class="column" v-if="activeFields.repeatAfter">
<!-- Repeat after --> <!-- Repeat after -->
@ -145,6 +158,7 @@
<repeat-after <repeat-after
v-model="task" v-model="task"
@change="saveTask" @change="saveTask"
:disabled="!canWrite"
ref="repeatAfter"/> ref="repeatAfter"/>
</div> </div>
<div class="column" v-if="activeFields.color"> <div class="column" v-if="activeFields.color">
@ -169,7 +183,7 @@
</span> </span>
Labels Labels
</div> </div>
<edit-labels :task-id="taskId" v-model="task.labels" ref="labels"/> <edit-labels :task-id="taskId" v-model="task.labels" ref="labels" :disabled="!canWrite"/>
</div> </div>
<!-- Description --> <!-- Description -->
@ -185,6 +199,7 @@
@change="saveTask" @change="saveTask"
:upload-enabled="true" :upload-enabled="true"
:upload-callback="attachmentUpload" :upload-callback="attachmentUpload"
:is-edit-enabled="canWrite"
placeholder="Click here to enter a description..."/> placeholder="Click here to enter a description..."/>
</div> </div>
@ -193,6 +208,7 @@
<attachments <attachments
:task-id="taskId" :task-id="taskId"
ref="attachments" ref="attachments"
:edit-enabled="canWrite"
/> />
</div> </div>
@ -210,6 +226,7 @@
:initial-related-tasks="task.relatedTasks" :initial-related-tasks="task.relatedTasks"
:show-no-relations-notice="true" :show-no-relations-notice="true"
ref="relatedTasks" ref="relatedTasks"
:edit-enabled="canWrite"
/> />
</div> </div>
@ -229,9 +246,9 @@
</div> </div>
<!-- Comments --> <!-- Comments -->
<comments :task-id="taskId"/> <comments :task-id="taskId" :can-write="canWrite"/>
</div> </div>
<div class="column is-one-third action-buttons"> <div class="column is-one-third action-buttons" v-if="canWrite">
<a <a
class="button is-outlined noshadow has-no-border" class="button is-outlined noshadow has-no-border"
:class="{'is-success': !task.done}" :class="{'is-success': !task.done}"
@ -244,11 +261,19 @@
Done! Done!
</template> </template>
</a> </a>
<a class="button" @click="setFieldActive('assignees')" v-shortkey="['ctrl', 'shift', 'a']" @shortkey="setFieldActive('assignees')"> <a
class="button"
@click="setFieldActive('assignees')"
v-shortkey="['ctrl', 'shift', 'a']"
@shortkey="setFieldActive('assignees')">
<span class="icon is-small"><icon icon="users"/></span> <span class="icon is-small"><icon icon="users"/></span>
Assign this task to a user Assign this task to a user
</a> </a>
<a class="button" @click="setFieldActive('labels')" v-shortkey="['ctrl', 'shift', 'l']" @shortkey="setFieldActive('labels')"> <a
class="button"
@click="setFieldActive('labels')"
v-shortkey="['ctrl', 'shift', 'l']"
@shortkey="setFieldActive('labels')">
<span class="icon is-small"><icon icon="tags"/></span> <span class="icon is-small"><icon icon="tags"/></span>
Add labels Add labels
</a> </a>
@ -256,7 +281,11 @@
<span class="icon is-small"><icon icon="history"/></span> <span class="icon is-small"><icon icon="history"/></span>
Set Reminders Set Reminders
</a> </a>
<a class="button" @click="setFieldActive('dueDate')" v-shortkey="['ctrl', 'shift', 'd']" @shortkey="setFieldActive('dueDate')"> <a
class="button"
@click="setFieldActive('dueDate')"
v-shortkey="['ctrl', 'shift', 'd']"
@shortkey="setFieldActive('dueDate')">
<span class="icon is-small"><icon icon="calendar"/></span> <span class="icon is-small"><icon icon="calendar"/></span>
Set Due Date Set Due Date
</a> </a>
@ -280,11 +309,19 @@
<span class="icon is-small"><icon icon="percent"/></span> <span class="icon is-small"><icon icon="percent"/></span>
Set Percent Done Set Percent Done
</a> </a>
<a class="button" @click="setFieldActive('attachments')" v-shortkey="['ctrl', 'shift', 'f']" @shortkey="setFieldActive('attachments')"> <a
class="button"
@click="setFieldActive('attachments')"
v-shortkey="['ctrl', 'shift', 'f']"
@shortkey="setFieldActive('attachments')">
<span class="icon is-small"><icon icon="paperclip"/></span> <span class="icon is-small"><icon icon="paperclip"/></span>
Add attachments Add attachments
</a> </a>
<a class="button" @click="setFieldActive('relatedTasks')" v-shortkey="['ctrl', 'shift', 'r']" @shortkey="setFieldActive('relatedTasks')"> <a
class="button"
@click="setFieldActive('relatedTasks')"
v-shortkey="['ctrl', 'shift', 'r']"
@shortkey="setFieldActive('relatedTasks')">
<span class="icon is-small"><icon icon="tasks"/></span> <span class="icon is-small"><icon icon="tasks"/></span>
Add task relations Add task relations
</a> </a>
@ -326,6 +363,7 @@
import relationKinds from '../../models/relationKinds.json' import relationKinds from '../../models/relationKinds.json'
import priorites from '../../models/priorities.json' import priorites from '../../models/priorities.json'
import rights from '../../models/rights.json'
import flatPickr from 'vue-flatpickr-component' import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css' import 'flatpickr/dist/flatpickr.css'
@ -419,7 +457,7 @@
} }
}, },
watch: { watch: {
'$route': 'loadTask' '$route': 'loadTask',
}, },
created() { created() {
this.taskService = new TaskService() this.taskService = new TaskService()
@ -444,11 +482,14 @@
} }
} }
if (!this.$store.getters["namespaces/getListAndNamespaceById"]) { if (!this.$store.getters['namespaces/getListAndNamespaceById']) {
return null return null
} }
return this.$store.getters["namespaces/getListAndNamespaceById"](this.task.listId) return this.$store.getters['namespaces/getListAndNamespaceById'](this.task.listId)
},
canWrite() {
return this.task && this.task.maxRight && this.task.maxRight > rights.READ
}, },
}, },
methods: { methods: {
@ -507,6 +548,10 @@
}, },
saveTask(undoCallback = null) { saveTask(undoCallback = null) {
if (!this.canWrite) {
return
}
this.task.dueDate = this.dueDate this.task.dueDate = this.dueDate
this.task.hexColor = this.taskColor this.task.hexColor = this.taskColor
@ -536,7 +581,11 @@
}, },
setFieldActive(fieldName) { setFieldActive(fieldName) {
this.activeFields[fieldName] = true this.activeFields[fieldName] = true
this.$nextTick(() => this.$refs[fieldName].$el.focus()) this.$nextTick(() => {
if (this.$refs[fieldName]) {
this.$refs[fieldName].$el.focus()
}
})
}, },
deleteTask() { deleteTask() {
this.$store.dispatch('tasks/delete', this.task) this.$store.dispatch('tasks/delete', this.task)

View file

@ -61,7 +61,6 @@
</div> </div>
</div> </div>
<div class="card is-fullwidth"> <div class="card is-fullwidth">
<header class="card-header"> <header class="card-header">
<p class="card-header-title"> <p class="card-header-title">
Team Members Team Members
@ -188,6 +187,7 @@
import UserService from '../../services/user' import UserService from '../../services/user'
import LoadingComponent from '../../components/misc/loading' import LoadingComponent from '../../components/misc/loading'
import ErrorComponent from '../../components/misc/error' import ErrorComponent from '../../components/misc/error'
import Rights from '../../models/rights.json'
export default { export default {
name: 'EditTeam', name: 'EditTeam',
@ -201,7 +201,6 @@
showDeleteModal: false, showDeleteModal: false,
showUserDeleteModal: false, showUserDeleteModal: false,
userIsAdmin: false,
newMember: UserModel, newMember: UserModel,
foundUsers: [], foundUsers: [],
@ -234,9 +233,14 @@
// call again the method if the route changes // call again the method if the route changes
'$route': 'loadTeam', '$route': 'loadTeam',
}, },
computed: mapState({ computed: {
userIsAdmin() {
return this.team && this.team.maxRight && this.team.maxRight > Rights.READ
},
...mapState({
userInfo: state => state.auth.info, userInfo: state => state.auth.info,
}), }),
},
methods: { methods: {
loadTeam() { loadTeam() {
this.team = new TeamModel({id: this.teamId}) this.team = new TeamModel({id: this.teamId})
@ -244,13 +248,6 @@
.then(response => { .then(response => {
this.$set(this, 'team', response) this.$set(this, 'team', response)
this.setTitle(`Edit Team ${this.team.name}`) this.setTitle(`Edit Team ${this.team.name}`)
let members = response.members
for (const m in members) {
members[m].teamId = this.teamId
if (members[m].id === this.userInfo.id && members[m].admin) {
this.userIsAdmin = true
}
}
}) })
.catch(e => { .catch(e => {
this.error(e, this) this.error(e, this)
@ -357,5 +354,9 @@
.team-members { .team-members {
padding: 0; padding: 0;
.table {
border-top: 0;
}
} }
</style> </style>