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:
parent
e64b4e3329
commit
3c07c6e8c0
22 changed files with 282 additions and 132 deletions
|
@ -34,7 +34,10 @@
|
|||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }">
|
||||
{{ currentList.title === '' ? 'Loading...': currentList.title}}
|
||||
</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"/>
|
||||
</router-link>
|
||||
</div>
|
||||
|
@ -291,6 +294,7 @@
|
|||
|
||||
import NamespaceService from './services/namespace'
|
||||
import authTypes from './models/authTypes'
|
||||
import Rights from './models/rights.json'
|
||||
|
||||
import swEvents from './ServiceWorker/events'
|
||||
import Notification from './components/misc/notification'
|
||||
|
@ -427,6 +431,7 @@
|
|||
background: 'background',
|
||||
imprintUrl: state => state.config.legal.imprintUrl,
|
||||
privacyPolicyUrl: state => state.config.legal.privacyPolicyUrl,
|
||||
canWriteCurrentList: state => state.currentList.maxRight > Rights.READ,
|
||||
}),
|
||||
methods: {
|
||||
logout() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="editor">
|
||||
<div class="tabs is-right" v-if="hasPreview">
|
||||
<div class="editor" :class="{'is-pulled-up': isEditEnabled}">
|
||||
<div class="tabs is-right" v-if="hasPreview && isEditEnabled">
|
||||
<ul>
|
||||
<li :class="{'is-active': isPreviewActive}" v-if="isEditActive">
|
||||
<a @click="showPreview">Preview</a>
|
||||
|
@ -58,6 +58,9 @@
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isEditEnabled: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
'has-dark-text': colorIsDark(t.hexColor)
|
||||
}"
|
||||
:style="{'border-color': t.hexColor, 'background-color': t.hexColor}"
|
||||
:isActive="true"
|
||||
:isActive="canWrite"
|
||||
:x="t.offsetDays * dayWidth - 6"
|
||||
:y="0"
|
||||
: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)'}">
|
||||
<VueDragResize
|
||||
class="task nodate"
|
||||
:isActive="true"
|
||||
:isActive="canWrite"
|
||||
:x="dayOffsetUntilToday * dayWidth - 6"
|
||||
:y="0"
|
||||
:h="31"
|
||||
|
@ -88,7 +88,7 @@
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<form @submit.prevent="addNewTask()" class="add-new-task">
|
||||
<form @submit.prevent="addNewTask()" class="add-new-task" v-if="canWrite">
|
||||
<transition name="width">
|
||||
<input
|
||||
type="text"
|
||||
|
@ -138,6 +138,8 @@
|
|||
import priorities from '../../models/priorities'
|
||||
import PriorityLabel from './partials/priorityLabel'
|
||||
import TaskCollectionService from '../../services/taskCollection'
|
||||
import {mapState} from 'vuex'
|
||||
import Rights from '../../models/rights.json'
|
||||
|
||||
export default {
|
||||
name: 'GanttChart',
|
||||
|
@ -201,6 +203,9 @@
|
|||
mounted() {
|
||||
this.buildTheGanttChart()
|
||||
},
|
||||
computed: mapState({
|
||||
canWrite: state => state.currentList.maxRight > Rights.READ,
|
||||
}),
|
||||
methods: {
|
||||
buildTheGanttChart() {
|
||||
this.setDates()
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
</span>
|
||||
Attachments
|
||||
<a
|
||||
v-if="editEnabled"
|
||||
class="button is-primary is-outlined is-small noshadow"
|
||||
@click="$refs.files.click()"
|
||||
:disabled="attachmentService.loading">
|
||||
|
@ -14,7 +15,7 @@
|
|||
</a>
|
||||
</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>
|
||||
|
||||
<table>
|
||||
|
@ -41,7 +42,7 @@
|
|||
<icon icon="cloud-download-alt"/>
|
||||
</span>
|
||||
</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">
|
||||
<icon icon="trash-alt"/>
|
||||
</span>
|
||||
|
@ -52,7 +53,7 @@
|
|||
</table>
|
||||
|
||||
<!-- Dropzone -->
|
||||
<div class="dropzone" :class="{ 'hidden': !showDropzone }">
|
||||
<div class="dropzone" :class="{ 'hidden': !showDropzone }" v-if="editEnabled">
|
||||
<div class="drop-hint">
|
||||
<div class="icon">
|
||||
<icon icon="cloud-upload-alt"/>
|
||||
|
@ -102,7 +103,10 @@
|
|||
},
|
||||
initialAttachments: {
|
||||
type: Array,
|
||||
}
|
||||
},
|
||||
editEnabled: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.attachmentService = new AttachmentService()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="content details has-top-border">
|
||||
<h1>
|
||||
<div class="content details" :class="{'has-top-border': canWrite || comments.length > 0}">
|
||||
<h1 v-if="canWrite || comments.length > 0">
|
||||
<span class="icon is-grey">
|
||||
<icon :icon="['far', 'comments']"/>
|
||||
</span>
|
||||
|
@ -15,7 +15,7 @@
|
|||
<img class="image is-avatar" :src="c.author.getAvatarUrl(48)" alt="" width="48" height="48"/>
|
||||
</figure>
|
||||
<div class="media-content">
|
||||
<div class="comment-info">
|
||||
<div class="comment-info" :class="{'is-pulled-up': canWrite}">
|
||||
<strong>{{ c.author.username }}</strong>
|
||||
<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)"> ·
|
||||
|
@ -27,13 +27,14 @@
|
|||
@change="() => {toggleEdit(c);editComment()}"
|
||||
:upload-enabled="true"
|
||||
: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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="media comment">
|
||||
<div class="media comment" v-if="canWrite">
|
||||
<figure class="media-left">
|
||||
<img class="image is-avatar" :src="userAvatar" alt="" width="48" height="48"/>
|
||||
</figure>
|
||||
|
@ -95,7 +96,10 @@
|
|||
taskId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
canWrite: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -125,7 +129,7 @@
|
|||
watch: {
|
||||
taskId() {
|
||||
this.loadComments()
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
userAvatar() {
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
track-by="id"
|
||||
select-label="Assign this user"
|
||||
:showNoOptions="false"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template slot="tag" slot-scope="{ option }">
|
||||
<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"/>
|
||||
</a>
|
||||
</template>
|
||||
|
@ -65,7 +66,10 @@
|
|||
initialAssignees: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
:showNoOptions="false"
|
||||
@tag="createAndAddLabel"
|
||||
tag-placeholder="Add this as new label"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<template slot="tag" slot-scope="{ option }">
|
||||
<span class="tag"
|
||||
|
@ -54,6 +55,9 @@
|
|||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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.1">10%</option>
|
||||
<option value="0.2">20%</option>
|
||||
|
@ -28,7 +28,10 @@
|
|||
value: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Set the priority to the :value every time it changes from the outside
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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.LOW">Low</option>
|
||||
<option :value="priorities.MEDIUM">Medium</option>
|
||||
|
@ -26,7 +26,10 @@
|
|||
value: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Set the priority to the :value every time it changes from the outside
|
||||
|
|
|
@ -1,47 +1,49 @@
|
|||
<template>
|
||||
<div class="task-relations">
|
||||
<label class="label">New Task Relation</label>
|
||||
<div class="field">
|
||||
<multiselect
|
||||
v-model="newTaskRelationTask"
|
||||
:options="foundTasks"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:loading="taskService.loading"
|
||||
:internal-search="true"
|
||||
@search-change="findTasks"
|
||||
placeholder="Type search for a new task to add as related..."
|
||||
label="title"
|
||||
track-by="id"
|
||||
:taggable="true"
|
||||
:showNoOptions="false"
|
||||
@tag="createAndRelateTask"
|
||||
tag-placeholder="Add this as new related task"
|
||||
>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
class="multiselect__clear"
|
||||
v-if="newTaskRelationTask !== null && newTaskRelationTask.id !== 0"
|
||||
@mousedown.prevent.stop="clearAllFoundTasks(props.search)"></div>
|
||||
</template>
|
||||
<span slot="noResult">No task found. Consider changing the search query.</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth has-defaults">
|
||||
<select v-model="newTaskRelationKind">
|
||||
<option value="unset">Select a relation kind</option>
|
||||
<option v-for="(label, rk) in relationKinds" :key="rk" :value="rk">
|
||||
{{ label[0] }}
|
||||
</option>
|
||||
</select>
|
||||
<template v-if="editEnabled">
|
||||
<label class="label">New Task Relation</label>
|
||||
<div class="field">
|
||||
<multiselect
|
||||
v-model="newTaskRelationTask"
|
||||
:options="foundTasks"
|
||||
:multiple="false"
|
||||
:searchable="true"
|
||||
:loading="taskService.loading"
|
||||
:internal-search="true"
|
||||
@search-change="findTasks"
|
||||
placeholder="Type search for a new task to add as related..."
|
||||
label="title"
|
||||
track-by="id"
|
||||
:taggable="true"
|
||||
:showNoOptions="false"
|
||||
@tag="createAndRelateTask"
|
||||
tag-placeholder="Add this as new related task"
|
||||
>
|
||||
<template slot="clear" slot-scope="props">
|
||||
<div
|
||||
class="multiselect__clear"
|
||||
v-if="newTaskRelationTask !== null && newTaskRelationTask.id !== 0"
|
||||
@mousedown.prevent.stop="clearAllFoundTasks(props.search)"></div>
|
||||
</template>
|
||||
<span slot="noResult">No task found. Consider changing the search query.</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth has-defaults">
|
||||
<select v-model="newTaskRelationKind">
|
||||
<option value="unset">Select a relation kind</option>
|
||||
<option v-for="(label, rk) in relationKinds" :key="rk" :value="rk">
|
||||
{{ label[0] }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-primary" @click="addTaskRelation()">Add task Relation</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<a class="button is-primary" @click="addTaskRelation()">Add task Relation</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="related-tasks" v-for="(rts, kind ) in relatedTasks" :key="kind">
|
||||
<template v-if="rts.length > 0">
|
||||
|
@ -50,13 +52,17 @@
|
|||
<div class="task" v-for="t in rts" :key="t.id">
|
||||
<router-link :to="{ name: $route.name, params: { id: t.id } }">
|
||||
<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 }} >
|
||||
</span>
|
||||
{{t.title}}
|
||||
</span>
|
||||
</router-link>
|
||||
<a
|
||||
v-if="editEnabled"
|
||||
class="remove"
|
||||
@click="() => {showDeleteModal = true; relationToDelete = {relationKind: kind, otherTaskId: t.id}}">
|
||||
<icon icon="trash-alt"/>
|
||||
|
@ -131,7 +137,10 @@
|
|||
listId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
}
|
||||
},
|
||||
editEnabled: {
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
|
@ -222,7 +231,7 @@
|
|||
return relationKinds[kind][1]
|
||||
}
|
||||
return relationKinds[kind][0]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -10,9 +10,10 @@
|
|||
:value="r"
|
||||
:data-index="index"
|
||||
placeholder="Add a new reminder..."
|
||||
:disabled="disabled"
|
||||
>
|
||||
</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>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -44,7 +45,10 @@
|
|||
value: {
|
||||
default: () => [],
|
||||
type: Array,
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
flatPickr,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<div class="column is-7 field has-addons">
|
||||
<div class="control">
|
||||
<input
|
||||
:disabled="disabled"
|
||||
class="input"
|
||||
placeholder="Specify an amount..."
|
||||
v-model="repeatAfter.amount"
|
||||
|
@ -13,7 +14,7 @@
|
|||
</div>
|
||||
<div class="control">
|
||||
<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="days">Days</option>
|
||||
<option value="weeks">Weeks</option>
|
||||
|
@ -24,6 +25,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<fancycheckbox
|
||||
:disabled="disabled"
|
||||
class="column"
|
||||
@change="updateData"
|
||||
v-model="task.repeatFromCurrentDate"
|
||||
|
@ -54,7 +56,10 @@
|
|||
default: () => {
|
||||
},
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(newVal) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<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}">
|
||||
<router-link :to="{ name: taskDetailRoute, params: { id: task.id } }">
|
||||
<router-link
|
||||
|
@ -88,6 +88,10 @@
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
theTask(newVal) {
|
||||
|
|
|
@ -3,6 +3,12 @@ import {objectToCamelCase} from '../helpers/case'
|
|||
|
||||
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.
|
||||
* @param data
|
||||
|
|
|
@ -310,7 +310,9 @@ export default class AbstractService {
|
|||
return this.errorHandler(error)
|
||||
})
|
||||
.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(() => {
|
||||
cancel()
|
||||
|
@ -377,7 +379,11 @@ export default class AbstractService {
|
|||
return this.errorHandler(error)
|
||||
})
|
||||
.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(() => {
|
||||
cancel()
|
||||
|
@ -399,7 +405,11 @@ export default class AbstractService {
|
|||
return this.errorHandler(error)
|
||||
})
|
||||
.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(() => {
|
||||
cancel()
|
||||
|
|
|
@ -347,7 +347,6 @@ fieldset[disabled] .multiselect {
|
|||
}
|
||||
|
||||
.multiselect--disabled {
|
||||
background: $multiselect-disabled;
|
||||
pointer-events: none;
|
||||
.multiselect__current, .multiselect__select {
|
||||
background: $multiselect-disabled;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
}
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
.comment-info {
|
||||
.comment-info.is-pulled-up {
|
||||
margin-bottom: -3rem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,10 @@
|
|||
}
|
||||
|
||||
&.description .editor {
|
||||
margin-top: -3.5rem;
|
||||
|
||||
&.is-pulled-up {
|
||||
margin-top: -3.5rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin-bottom: 0;
|
||||
|
@ -139,15 +142,17 @@
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
&:hover, &:active {
|
||||
background: $input-background-color;
|
||||
border-color: $input-border-color;
|
||||
cursor: text;
|
||||
}
|
||||
&:not(:disabled) {
|
||||
&:hover, &:active {
|
||||
background: $input-background-color;
|
||||
border-color: $input-border-color;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: $input-background-color;
|
||||
border-color: $input-focus-border-color;
|
||||
&:focus {
|
||||
background: $input-background-color;
|
||||
border-color: $input-focus-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
@focusout="() => saveBucketTitle(bucket.id)"
|
||||
:ref="`bucket${bucket.id}title`"
|
||||
@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)">
|
||||
<span class="icon">
|
||||
<icon icon="ellipsis-v"/>
|
||||
|
@ -31,7 +35,9 @@
|
|||
</div>
|
||||
</div>
|
||||
<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)"
|
||||
group-name="buckets"
|
||||
:get-child-payload="getTaskPayload(bucket.id)"
|
||||
|
@ -41,7 +47,12 @@
|
|||
drag-class-drop="ghost-task-drop"
|
||||
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
|
||||
class="task loader-container draggable"
|
||||
:class="{
|
||||
|
@ -103,10 +114,10 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Draggable>
|
||||
</Container>
|
||||
</component>
|
||||
</component>
|
||||
</div>
|
||||
<div class="bucket-footer">
|
||||
<div class="bucket-footer" v-if="canWrite">
|
||||
<div class="field" v-if="showNewTaskInput[bucket.id]">
|
||||
<div class="control">
|
||||
<input
|
||||
|
@ -144,7 +155,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bucket new-bucket" v-if="!loading">
|
||||
<div class="bucket new-bucket" v-if="!loading && canWrite">
|
||||
<input
|
||||
v-if="showNewBucketInput"
|
||||
class="input"
|
||||
|
@ -204,6 +215,7 @@
|
|||
import {mapState} from 'vuex'
|
||||
import {LOADING} from '../../../store/mutation-types'
|
||||
import {saveListView} from '../../../helpers/saveListView'
|
||||
import Rights from '../../../models/rights.json'
|
||||
|
||||
export default {
|
||||
name: 'Kanban',
|
||||
|
@ -254,6 +266,7 @@
|
|||
buckets: state => state.kanban.buckets,
|
||||
loadedListId: state => state.kanban.listId,
|
||||
loading: LOADING,
|
||||
canWrite: state => state.currentList.maxRight > Rights.READ,
|
||||
}),
|
||||
methods: {
|
||||
loadBuckets() {
|
||||
|
@ -385,7 +398,7 @@
|
|||
const task = new TaskModel({
|
||||
title: this.newTaskText,
|
||||
bucketId: this.buckets[bi].id,
|
||||
listId: this.$route.params.listId
|
||||
listId: this.$route.params.listId,
|
||||
})
|
||||
|
||||
this.taskService.create(task)
|
||||
|
@ -410,7 +423,7 @@
|
|||
|
||||
const newBucket = new BucketModel({
|
||||
title: this.newBucketTitle,
|
||||
listId: parseInt(this.$route.params.listId)
|
||||
listId: parseInt(this.$route.params.listId),
|
||||
})
|
||||
|
||||
this.$store.dispatch('kanban/createBucket', newBucket)
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
</transition>
|
||||
</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">
|
||||
<p class="control has-icons-left is-expanded" :class="{ 'is-loading': taskService.loading}">
|
||||
<input
|
||||
|
@ -85,8 +85,13 @@
|
|||
<div class="column">
|
||||
<div class="tasks" v-if="tasks && tasks.length > 0" :class="{'short': isTaskEdit}">
|
||||
<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"/>
|
||||
<div @click="editTask(t.id)" class="icon settings" v-if="!list.isArchived">
|
||||
<single-task-in-list
|
||||
: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"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -169,6 +174,8 @@
|
|||
import taskList from '../../../components/tasks/mixins/taskList'
|
||||
import {saveListView} from '../../../helpers/saveListView'
|
||||
import Filters from '../../../components/list/partials/filters'
|
||||
import Rights from '../../../models/rights.json'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'List',
|
||||
|
@ -202,6 +209,9 @@
|
|||
// We use local storage and not vuex here to make it persistent across reloads.
|
||||
saveListView(this.$route.params.listId, this.$route.name)
|
||||
},
|
||||
computed: mapState({
|
||||
canWrite: state => state.currentList.maxRight > Rights.READ,
|
||||
}),
|
||||
methods: {
|
||||
// This function initializes the tasks page and loads the first page of tasks
|
||||
initTasks(page, search = '') {
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<!-- Content and buttons -->
|
||||
<div class="columns">
|
||||
<!-- Content -->
|
||||
<div class="column is-two-thirds">
|
||||
<div class="column" :class="{'is-two-thirds': canWrite}">
|
||||
<div class="columns details">
|
||||
<div class="column assignees" v-if="activeFields.assignees">
|
||||
<!-- Assignees -->
|
||||
|
@ -35,6 +35,7 @@
|
|||
:list-id="task.listId"
|
||||
:initial-assignees="task.assignees"
|
||||
ref="assignees"
|
||||
:disabled="!canWrite"
|
||||
/>
|
||||
</div>
|
||||
<div class="column" v-if="activeFields.priority">
|
||||
|
@ -43,7 +44,11 @@
|
|||
<icon :icon="['far', 'star']"/>
|
||||
Priority
|
||||
</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 class="column" v-if="activeFields.dueDate">
|
||||
<!-- Due Date -->
|
||||
|
@ -55,7 +60,7 @@
|
|||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
class="input"
|
||||
:disabled="taskService.loading"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
v-model="dueDate"
|
||||
:config="flatPickerConfig"
|
||||
@on-close="saveTask"
|
||||
|
@ -63,7 +68,7 @@
|
|||
ref="dueDate"
|
||||
>
|
||||
</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">
|
||||
<icon icon="times"></icon>
|
||||
</span>
|
||||
|
@ -76,7 +81,11 @@
|
|||
<icon icon="percent"/>
|
||||
Percent Done
|
||||
</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 class="column" v-if="activeFields.startDate">
|
||||
<!-- Start Date -->
|
||||
|
@ -88,7 +97,7 @@
|
|||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
class="input"
|
||||
:disabled="taskService.loading"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
v-model="task.startDate"
|
||||
:config="flatPickerConfig"
|
||||
@on-close="saveTask"
|
||||
|
@ -96,7 +105,7 @@
|
|||
ref="startDate"
|
||||
>
|
||||
</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">
|
||||
<icon icon="times"></icon>
|
||||
</span>
|
||||
|
@ -113,7 +122,7 @@
|
|||
<flat-pickr
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
class="input"
|
||||
:disabled="taskService.loading"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
v-model="task.endDate"
|
||||
:config="flatPickerConfig"
|
||||
@on-close="saveTask"
|
||||
|
@ -121,7 +130,7 @@
|
|||
ref="endDate"
|
||||
>
|
||||
</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">
|
||||
<icon icon="times"></icon>
|
||||
</span>
|
||||
|
@ -134,7 +143,11 @@
|
|||
<icon icon="history"/>
|
||||
Reminders
|
||||
</div>
|
||||
<reminders v-model="task.reminderDates" @change="saveTask" ref="reminders"/>
|
||||
<reminders
|
||||
v-model="task.reminderDates"
|
||||
@change="saveTask"
|
||||
ref="reminders"
|
||||
:disabled="!canWrite"/>
|
||||
</div>
|
||||
<div class="column" v-if="activeFields.repeatAfter">
|
||||
<!-- Repeat after -->
|
||||
|
@ -145,6 +158,7 @@
|
|||
<repeat-after
|
||||
v-model="task"
|
||||
@change="saveTask"
|
||||
:disabled="!canWrite"
|
||||
ref="repeatAfter"/>
|
||||
</div>
|
||||
<div class="column" v-if="activeFields.color">
|
||||
|
@ -169,7 +183,7 @@
|
|||
</span>
|
||||
Labels
|
||||
</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>
|
||||
|
||||
<!-- Description -->
|
||||
|
@ -185,6 +199,7 @@
|
|||
@change="saveTask"
|
||||
:upload-enabled="true"
|
||||
:upload-callback="attachmentUpload"
|
||||
:is-edit-enabled="canWrite"
|
||||
placeholder="Click here to enter a description..."/>
|
||||
</div>
|
||||
|
||||
|
@ -193,6 +208,7 @@
|
|||
<attachments
|
||||
:task-id="taskId"
|
||||
ref="attachments"
|
||||
:edit-enabled="canWrite"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -210,6 +226,7 @@
|
|||
:initial-related-tasks="task.relatedTasks"
|
||||
:show-no-relations-notice="true"
|
||||
ref="relatedTasks"
|
||||
:edit-enabled="canWrite"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -229,9 +246,9 @@
|
|||
</div>
|
||||
|
||||
<!-- Comments -->
|
||||
<comments :task-id="taskId"/>
|
||||
<comments :task-id="taskId" :can-write="canWrite"/>
|
||||
</div>
|
||||
<div class="column is-one-third action-buttons">
|
||||
<div class="column is-one-third action-buttons" v-if="canWrite">
|
||||
<a
|
||||
class="button is-outlined noshadow has-no-border"
|
||||
:class="{'is-success': !task.done}"
|
||||
|
@ -244,11 +261,19 @@
|
|||
Done!
|
||||
</template>
|
||||
</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>
|
||||
Assign this task to a user
|
||||
</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>
|
||||
Add labels
|
||||
</a>
|
||||
|
@ -256,7 +281,11 @@
|
|||
<span class="icon is-small"><icon icon="history"/></span>
|
||||
Set Reminders
|
||||
</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>
|
||||
Set Due Date
|
||||
</a>
|
||||
|
@ -280,11 +309,19 @@
|
|||
<span class="icon is-small"><icon icon="percent"/></span>
|
||||
Set Percent Done
|
||||
</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>
|
||||
Add attachments
|
||||
</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>
|
||||
Add task relations
|
||||
</a>
|
||||
|
@ -326,6 +363,7 @@
|
|||
import relationKinds from '../../models/relationKinds.json'
|
||||
|
||||
import priorites from '../../models/priorities.json'
|
||||
import rights from '../../models/rights.json'
|
||||
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
@ -419,7 +457,7 @@
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
'$route': 'loadTask'
|
||||
'$route': 'loadTask',
|
||||
},
|
||||
created() {
|
||||
this.taskService = new TaskService()
|
||||
|
@ -444,11 +482,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.$store.getters["namespaces/getListAndNamespaceById"]) {
|
||||
if (!this.$store.getters['namespaces/getListAndNamespaceById']) {
|
||||
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: {
|
||||
|
@ -507,6 +548,10 @@
|
|||
},
|
||||
saveTask(undoCallback = null) {
|
||||
|
||||
if (!this.canWrite) {
|
||||
return
|
||||
}
|
||||
|
||||
this.task.dueDate = this.dueDate
|
||||
this.task.hexColor = this.taskColor
|
||||
|
||||
|
@ -536,7 +581,11 @@
|
|||
},
|
||||
setFieldActive(fieldName) {
|
||||
this.activeFields[fieldName] = true
|
||||
this.$nextTick(() => this.$refs[fieldName].$el.focus())
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs[fieldName]) {
|
||||
this.$refs[fieldName].$el.focus()
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteTask() {
|
||||
this.$store.dispatch('tasks/delete', this.task)
|
||||
|
|
|
@ -61,7 +61,6 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="card is-fullwidth">
|
||||
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Team Members
|
||||
|
@ -188,6 +187,7 @@
|
|||
import UserService from '../../services/user'
|
||||
import LoadingComponent from '../../components/misc/loading'
|
||||
import ErrorComponent from '../../components/misc/error'
|
||||
import Rights from '../../models/rights.json'
|
||||
|
||||
export default {
|
||||
name: 'EditTeam',
|
||||
|
@ -201,7 +201,6 @@
|
|||
|
||||
showDeleteModal: false,
|
||||
showUserDeleteModal: false,
|
||||
userIsAdmin: false,
|
||||
|
||||
newMember: UserModel,
|
||||
foundUsers: [],
|
||||
|
@ -234,9 +233,14 @@
|
|||
// call again the method if the route changes
|
||||
'$route': 'loadTeam',
|
||||
},
|
||||
computed: mapState({
|
||||
userInfo: state => state.auth.info,
|
||||
}),
|
||||
computed: {
|
||||
userIsAdmin() {
|
||||
return this.team && this.team.maxRight && this.team.maxRight > Rights.READ
|
||||
},
|
||||
...mapState({
|
||||
userInfo: state => state.auth.info,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
loadTeam() {
|
||||
this.team = new TeamModel({id: this.teamId})
|
||||
|
@ -244,13 +248,6 @@
|
|||
.then(response => {
|
||||
this.$set(this, 'team', response)
|
||||
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 => {
|
||||
this.error(e, this)
|
||||
|
@ -313,7 +310,7 @@
|
|||
member.admin = !member.admin
|
||||
this.teamMemberService.update(member)
|
||||
.then(r => {
|
||||
for(const tm in this.team.members) {
|
||||
for (const tm in this.team.members) {
|
||||
if (this.team.members[tm].id === member.id) {
|
||||
this.$set(this.team.members[tm], 'admin', r.admin)
|
||||
break
|
||||
|
@ -357,5 +354,9 @@
|
|||
|
||||
.team-members {
|
||||
padding: 0;
|
||||
|
||||
.table {
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue