Better save messages for tasks (#307)
Add success messages when managing assignees Add success messages when managing labels Add better loading animations for related tasks Add better loading animations for comments Don't block everything while loading Move task heading to separate component which handles all saving related things Make sure to only show the loading spinner and saved message when saving the description Show a maximum of 2 notifications Move task description to separate component Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/307 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
parent
b9eeec0125
commit
148cc1dcca
11 changed files with 456 additions and 223 deletions
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<notifications position="bottom left">
|
<notifications position="bottom left" :max="2">
|
||||||
<template slot="body" slot-scope="props">
|
<template slot="body" slot-scope="props">
|
||||||
<div :class="['vue-notification-template', 'vue-notification', props.item.type]" @click="close(props)">
|
<div :class="['vue-notification-template', 'vue-notification', props.item.type]" @click="close(props)">
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -7,9 +7,10 @@
|
||||||
Comments
|
Comments
|
||||||
</h1>
|
</h1>
|
||||||
<div class="comments">
|
<div class="comments">
|
||||||
<progress class="progress is-small is-info" max="100" v-if="taskCommentService.loading">
|
<span class="is-inline-flex is-align-items-center" v-if="taskCommentService.loading && saving === null && !creating">
|
||||||
|
<span class="loader is-inline-block mr-2"></span>
|
||||||
Loading comments...
|
Loading comments...
|
||||||
</progress>
|
</span>
|
||||||
<div :key="c.id" class="media comment" v-for="c in comments">
|
<div :key="c.id" class="media comment" v-for="c in comments">
|
||||||
<figure class="media-left is-hidden-mobile">
|
<figure class="media-left is-hidden-mobile">
|
||||||
<img :src="c.author.getAvatarUrl(48)" alt="" class="image is-avatar" height="48" width="48"/>
|
<img :src="c.author.getAvatarUrl(48)" alt="" class="image is-avatar" height="48" width="48"/>
|
||||||
|
@ -22,6 +23,15 @@
|
||||||
<span v-if="+new Date(c.created) !== +new Date(c.updated)" v-tooltip="formatDate(c.updated)">
|
<span v-if="+new Date(c.created) !== +new Date(c.updated)" v-tooltip="formatDate(c.updated)">
|
||||||
· edited {{ formatDateSince(c.updated) }}
|
· edited {{ formatDateSince(c.updated) }}
|
||||||
</span>
|
</span>
|
||||||
|
<transition name="fade">
|
||||||
|
<span class="is-inline-flex" v-if="taskCommentService.loading && saving === c.id">
|
||||||
|
<span class="loader is-inline-block mr-2"></span>
|
||||||
|
Saving...
|
||||||
|
</span>
|
||||||
|
<span class="has-text-success" v-if="!taskCommentService.loading && saved === c.id">
|
||||||
|
Saved!
|
||||||
|
</span>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<editor
|
<editor
|
||||||
:has-preview="true"
|
:has-preview="true"
|
||||||
|
@ -41,6 +51,12 @@
|
||||||
</figure>
|
</figure>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<div class="form">
|
<div class="form">
|
||||||
|
<transition name="fade">
|
||||||
|
<span class="is-inline-flex" v-if="taskCommentService.loading && creating">
|
||||||
|
<span class="loader is-inline-block mr-2"></span>
|
||||||
|
Creating comment...
|
||||||
|
</span>
|
||||||
|
</transition>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<editor
|
<editor
|
||||||
:class="{'is-loading': taskCommentService.loading && !isCommentEdit}"
|
:class="{'is-loading': taskCommentService.loading && !isCommentEdit}"
|
||||||
|
@ -116,6 +132,10 @@ export default {
|
||||||
newComment: TaskCommentModel,
|
newComment: TaskCommentModel,
|
||||||
editorActive: true,
|
editorActive: true,
|
||||||
actions: {},
|
actions: {},
|
||||||
|
|
||||||
|
saved: null,
|
||||||
|
saving: null,
|
||||||
|
creating: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
@ -164,15 +184,20 @@ export default {
|
||||||
// See https://github.com/NikulinIlya/vue-easymde/issues/3
|
// See https://github.com/NikulinIlya/vue-easymde/issues/3
|
||||||
this.editorActive = false
|
this.editorActive = false
|
||||||
this.$nextTick(() => this.editorActive = true)
|
this.$nextTick(() => this.editorActive = true)
|
||||||
|
this.creating = true
|
||||||
|
|
||||||
this.taskCommentService.create(this.newComment)
|
this.taskCommentService.create(this.newComment)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
this.comments.push(r)
|
this.comments.push(r)
|
||||||
this.newComment.comment = ''
|
this.newComment.comment = ''
|
||||||
|
this.success({message: 'The comment was added successfully.'}, this)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.creating = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
toggleEdit(comment) {
|
toggleEdit(comment) {
|
||||||
this.isCommentEdit = !this.isCommentEdit
|
this.isCommentEdit = !this.isCommentEdit
|
||||||
|
@ -186,6 +211,9 @@ export default {
|
||||||
if (this.commentEdit.comment === '') {
|
if (this.commentEdit.comment === '') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.saving = this.commentEdit.id
|
||||||
|
|
||||||
this.commentEdit.taskId = this.taskId
|
this.commentEdit.taskId = this.taskId
|
||||||
this.taskCommentService.update(this.commentEdit)
|
this.taskCommentService.update(this.commentEdit)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
|
@ -194,12 +222,17 @@ export default {
|
||||||
this.$set(this.comments, c, r)
|
this.$set(this.comments, c, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.saved = this.commentEdit.id
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saved = null
|
||||||
|
}, 2000)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.isCommentEdit = false
|
this.isCommentEdit = false
|
||||||
|
this.saving = null
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
deleteComment() {
|
deleteComment() {
|
||||||
|
|
97
src/components/tasks/partials/description.vue
Normal file
97
src/components/tasks/partials/description.vue
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
<span class="icon is-grey">
|
||||||
|
<icon icon="align-left"/>
|
||||||
|
</span>
|
||||||
|
Description
|
||||||
|
<transition name="fade">
|
||||||
|
<span class="is-small is-inline-flex" v-if="loading && saving">
|
||||||
|
<span class="loader is-inline-block mr-2"></span>
|
||||||
|
Saving...
|
||||||
|
</span>
|
||||||
|
<span class="is-small has-text-success" v-if="!loading && saved">
|
||||||
|
<icon icon="check"/>
|
||||||
|
Saved!
|
||||||
|
</span>
|
||||||
|
</transition>
|
||||||
|
</h3>
|
||||||
|
<editor
|
||||||
|
:is-edit-enabled="canWrite"
|
||||||
|
:upload-callback="attachmentUpload"
|
||||||
|
:upload-enabled="true"
|
||||||
|
@change="save"
|
||||||
|
placeholder="Click here to enter a description..."
|
||||||
|
v-model="task.description"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import LoadingComponent from '@/components/misc/loading'
|
||||||
|
import ErrorComponent from '@/components/misc/error'
|
||||||
|
|
||||||
|
import {LOADING} from '@/store/mutation-types'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'description',
|
||||||
|
components: {
|
||||||
|
editor: () => ({
|
||||||
|
component: import(/* webpackChunkName: "editor" */ '@/components/input/editor'),
|
||||||
|
loading: LoadingComponent,
|
||||||
|
error: ErrorComponent,
|
||||||
|
timeout: 60000,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
task: {description: ''},
|
||||||
|
saved: false,
|
||||||
|
saving: false, // Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: mapState({
|
||||||
|
loading: LOADING,
|
||||||
|
}),
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
attachmentUpload: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
canWrite: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(newVal) {
|
||||||
|
this.task = newVal
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.task = this.value
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
save() {
|
||||||
|
this.saving = true
|
||||||
|
|
||||||
|
this.$store.dispatch('tasks/update', this.task)
|
||||||
|
.then(() => {
|
||||||
|
this.$emit('input', this.task)
|
||||||
|
this.saved = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saved = false
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
this.error(e, this)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.saving = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -97,6 +97,7 @@ export default {
|
||||||
this.$store.dispatch('tasks/addAssignee', {user: user, taskId: this.taskId})
|
this.$store.dispatch('tasks/addAssignee', {user: user, taskId: this.taskId})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$emit('input', this.assignees)
|
this.$emit('input', this.assignees)
|
||||||
|
this.success({message: 'The user has been assigned successfully.'}, this)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
|
@ -111,6 +112,7 @@ export default {
|
||||||
this.assignees.splice(a, 1)
|
this.assignees.splice(a, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.success({message: 'The user has been unassinged successfully.'}, this)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
:showNoOptions="false"
|
:showNoOptions="false"
|
||||||
:taggable="true"
|
:taggable="true"
|
||||||
@search-change="findLabel"
|
@search-change="findLabel"
|
||||||
@select="addLabel"
|
@select="label => addLabel(label)"
|
||||||
@tag="createAndAddLabel"
|
@tag="createAndAddLabel"
|
||||||
label="title"
|
label="title"
|
||||||
placeholder="Type to add a new label..."
|
placeholder="Type to add a new label..."
|
||||||
|
@ -121,10 +121,13 @@ export default {
|
||||||
clearAllLabels() {
|
clearAllLabels() {
|
||||||
this.$set(this, 'foundLabels', [])
|
this.$set(this, 'foundLabels', [])
|
||||||
},
|
},
|
||||||
addLabel(label) {
|
addLabel(label, showNotification = true) {
|
||||||
this.$store.dispatch('tasks/addLabel', {label: label, taskId: this.taskId})
|
this.$store.dispatch('tasks/addLabel', {label: label, taskId: this.taskId})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$emit('input', this.labels)
|
this.$emit('input', this.labels)
|
||||||
|
if (showNotification) {
|
||||||
|
this.success({message: 'The label has been added successfully.'}, this)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
|
@ -140,6 +143,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$emit('input', this.labels)
|
this.$emit('input', this.labels)
|
||||||
|
this.success({message: 'The label has been removed successfully.'}, this)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
|
@ -149,8 +153,9 @@ export default {
|
||||||
let newLabel = new LabelModel({title: title})
|
let newLabel = new LabelModel({title: title})
|
||||||
this.labelService.create(newLabel)
|
this.labelService.create(newLabel)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
this.addLabel(r)
|
this.addLabel(r, false)
|
||||||
this.labels.push(r)
|
this.labels.push(r)
|
||||||
|
this.success({message: 'The label has been created successfully.'}, this)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
|
|
101
src/components/tasks/partials/heading.vue
Normal file
101
src/components/tasks/partials/heading.vue
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div class="heading">
|
||||||
|
<h1 class="title task-id" v-if="task.identifier === ''">
|
||||||
|
#{{ task.index }}
|
||||||
|
</h1>
|
||||||
|
<h1 class="title task-id" v-else>
|
||||||
|
{{ task.identifier }}
|
||||||
|
</h1>
|
||||||
|
<div class="is-done" v-if="task.done">Done</div>
|
||||||
|
<h1
|
||||||
|
@focusout="save()"
|
||||||
|
@keyup.ctrl.enter="save()"
|
||||||
|
class="title input"
|
||||||
|
contenteditable="true"
|
||||||
|
ref="taskTitle">
|
||||||
|
{{ task.title }}
|
||||||
|
</h1>
|
||||||
|
<transition name="fade">
|
||||||
|
<span class="is-inline-flex is-align-items-center" v-if="loading && saving">
|
||||||
|
<span class="loader is-inline-block mr-2"></span>
|
||||||
|
Saving...
|
||||||
|
</span>
|
||||||
|
<span class="has-text-success is-inline-flex is-align-content-center" v-if="!loading && saved">
|
||||||
|
<icon icon="check" class="mr-2"/>
|
||||||
|
Saved!
|
||||||
|
</span>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {LOADING} from '@/store/mutation-types'
|
||||||
|
import {mapState} from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'heading',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
task: {title: '', identifier: '', index:''},
|
||||||
|
taskTitle: '',
|
||||||
|
saved: false,
|
||||||
|
saving: false, // Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: mapState({
|
||||||
|
loading: LOADING,
|
||||||
|
}),
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value(newVal) {
|
||||||
|
this.task = newVal
|
||||||
|
this.taskTitle = this.task.title
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.task = this.value
|
||||||
|
this.taskTitle = this.task.title
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
save() {
|
||||||
|
this.$refs.taskTitle.spellcheck = false
|
||||||
|
|
||||||
|
// Pull the task title from the contenteditable
|
||||||
|
let taskTitle = this.$refs.taskTitle.textContent
|
||||||
|
this.task.title = taskTitle
|
||||||
|
|
||||||
|
// We only want to save if the title was actually change.
|
||||||
|
// Because the contenteditable does not have a change event,
|
||||||
|
// we're building it ourselves and only calling saveTask()
|
||||||
|
// if the task title changed.
|
||||||
|
if (this.task.title !== this.taskTitle) {
|
||||||
|
this.saveTask()
|
||||||
|
this.taskTitle = taskTitle
|
||||||
|
}
|
||||||
|
},
|
||||||
|
saveTask() {
|
||||||
|
this.saving = true
|
||||||
|
|
||||||
|
this.$store.dispatch('tasks/update', this.task)
|
||||||
|
.then(() => {
|
||||||
|
this.$emit('input', this.task)
|
||||||
|
this.saved = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saved = false
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
this.error(e, this)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.saving = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -1,7 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="task-relations">
|
<div class="task-relations">
|
||||||
<template v-if="editEnabled">
|
<template v-if="editEnabled">
|
||||||
<label class="label">New Task Relation</label>
|
<label class="label">
|
||||||
|
New Task Relation
|
||||||
|
<transition name="fade">
|
||||||
|
<span class="is-inline-flex" v-if="taskRelationService.loading">
|
||||||
|
<span class="loader is-inline-block mr-2"></span>
|
||||||
|
Saving...
|
||||||
|
</span>
|
||||||
|
<span class="has-text-success" v-if="!taskRelationService.loading && saved">
|
||||||
|
Saved!
|
||||||
|
</span>
|
||||||
|
</transition>
|
||||||
|
</label>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<multiselect
|
<multiselect
|
||||||
:internal-search="true"
|
:internal-search="true"
|
||||||
|
@ -112,6 +123,7 @@ export default {
|
||||||
taskRelationService: TaskRelationService,
|
taskRelationService: TaskRelationService,
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
relationToDelete: {},
|
relationToDelete: {},
|
||||||
|
saved: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -188,6 +200,10 @@ export default {
|
||||||
}
|
}
|
||||||
this.relatedTasks[this.newTaskRelationKind].push(this.newTaskRelationTask)
|
this.relatedTasks[this.newTaskRelationKind].push(this.newTaskRelationTask)
|
||||||
this.newTaskRelationTask = new TaskModel()
|
this.newTaskRelationTask = new TaskModel()
|
||||||
|
this.saved = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saved = false
|
||||||
|
}, 2000)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
|
@ -208,6 +224,10 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this.saved = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saved = false
|
||||||
|
}, 2000)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
|
|
|
@ -3,12 +3,15 @@ import TaskAssigneeService from '../../services/taskAssignee'
|
||||||
import TaskAssigneeModel from '../../models/taskAssignee'
|
import TaskAssigneeModel from '../../models/taskAssignee'
|
||||||
import LabelTaskModel from '../../models/labelTask'
|
import LabelTaskModel from '../../models/labelTask'
|
||||||
import LabelTaskService from '../../services/labelTask'
|
import LabelTaskService from '../../services/labelTask'
|
||||||
|
import {setLoading} from '@/store/helper'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: () => ({}),
|
state: () => ({}),
|
||||||
actions: {
|
actions: {
|
||||||
update(ctx, task) {
|
update(ctx, task) {
|
||||||
|
const cancel = setLoading(ctx)
|
||||||
|
|
||||||
const taskService = new TaskService()
|
const taskService = new TaskService()
|
||||||
return taskService.update(task)
|
return taskService.update(task)
|
||||||
.then(t => {
|
.then(t => {
|
||||||
|
@ -18,6 +21,9 @@ export default {
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
cancel()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
delete(ctx, task) {
|
delete(ctx, task) {
|
||||||
const taskService = new TaskService()
|
const taskService = new TaskService()
|
||||||
|
|
|
@ -44,3 +44,7 @@
|
||||||
.media-content {
|
.media-content {
|
||||||
width: calc(100% - 48px - 2em);
|
width: calc(100% - 48px - 2em);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content h3 .is-small {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<div :class="{ 'is-loading': loading}" class="kanban loader-container">
|
<div :class="{ 'is-loading': loading}" class="kanban loader-container">
|
||||||
<div :key="`bucket${bucket.id}`" class="bucket" v-for="bucket in buckets">
|
<div :key="`bucket${bucket.id}`" class="bucket" v-for="bucket in buckets">
|
||||||
<div class="bucket-header">
|
<div class="bucket-header">
|
||||||
|
@ -214,6 +215,7 @@
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
|
<!-- This router view is used to show the task popup while keeping the kanban board itself -->
|
||||||
<transition name="modal">
|
<transition name="modal">
|
||||||
|
|
|
@ -1,23 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="{ 'is-loading': taskService.loading}" class="loader-container task-view-container">
|
<div :class="{ 'is-loading': taskService.loading}" class="loader-container task-view-container">
|
||||||
<div class="task-view">
|
<div class="task-view">
|
||||||
<div class="heading">
|
<heading v-model="task"/>
|
||||||
<h1 class="title task-id" v-if="task.identifier === ''">
|
|
||||||
#{{ task.index }}
|
|
||||||
</h1>
|
|
||||||
<h1 class="title task-id" v-else>
|
|
||||||
{{ task.identifier }}
|
|
||||||
</h1>
|
|
||||||
<div class="is-done" v-if="task.done">Done</div>
|
|
||||||
<h1
|
|
||||||
@focusout="saveTaskOnChange()"
|
|
||||||
@keyup.ctrl.enter="saveTaskOnChange()"
|
|
||||||
class="title input"
|
|
||||||
contenteditable="true"
|
|
||||||
ref="taskTitle">
|
|
||||||
{{ task.title }}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<h6 class="subtitle" v-if="parent && parent.namespace && parent.list">
|
<h6 class="subtitle" v-if="parent && parent.namespace && parent.list">
|
||||||
{{ parent.namespace.title }} >
|
{{ parent.namespace.title }} >
|
||||||
<router-link :to="{ name: listViewName, params: { listId: parent.list.id } }">
|
<router-link :to="{ name: listViewName, params: { listId: parent.list.id } }">
|
||||||
|
@ -67,7 +51,7 @@
|
||||||
:class="{ 'disabled': taskService.loading}"
|
:class="{ 'disabled': taskService.loading}"
|
||||||
:config="flatPickerConfig"
|
:config="flatPickerConfig"
|
||||||
:disabled="taskService.loading || !canWrite"
|
:disabled="taskService.loading || !canWrite"
|
||||||
@on-close="saveTask"
|
@on-close="() => saveTask()"
|
||||||
class="input"
|
class="input"
|
||||||
placeholder="Click here to set a due date"
|
placeholder="Click here to set a due date"
|
||||||
ref="dueDate"
|
ref="dueDate"
|
||||||
|
@ -104,7 +88,7 @@
|
||||||
:class="{ 'disabled': taskService.loading}"
|
:class="{ 'disabled': taskService.loading}"
|
||||||
:config="flatPickerConfig"
|
:config="flatPickerConfig"
|
||||||
:disabled="taskService.loading || !canWrite"
|
:disabled="taskService.loading || !canWrite"
|
||||||
@on-close="saveTask"
|
@on-close="() => saveTask()"
|
||||||
class="input"
|
class="input"
|
||||||
placeholder="Click here to set a start date"
|
placeholder="Click here to set a start date"
|
||||||
ref="startDate"
|
ref="startDate"
|
||||||
|
@ -129,7 +113,7 @@
|
||||||
:class="{ 'disabled': taskService.loading}"
|
:class="{ 'disabled': taskService.loading}"
|
||||||
:config="flatPickerConfig"
|
:config="flatPickerConfig"
|
||||||
:disabled="taskService.loading || !canWrite"
|
:disabled="taskService.loading || !canWrite"
|
||||||
@on-close="saveTask"
|
@on-close="() => saveTask()"
|
||||||
class="input"
|
class="input"
|
||||||
placeholder="Click here to set an end date"
|
placeholder="Click here to set an end date"
|
||||||
ref="endDate"
|
ref="endDate"
|
||||||
|
@ -194,19 +178,11 @@
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<div :class="{ 'has-top-border': activeFields.labels }" class="details content description">
|
<div :class="{ 'has-top-border': activeFields.labels }" class="details content description">
|
||||||
<h3>
|
<description
|
||||||
<span class="icon is-grey">
|
v-model="task"
|
||||||
<icon icon="align-left"/>
|
:can-write="canWrite"
|
||||||
</span>
|
:attachment-upload="attachmentUpload"
|
||||||
Description
|
/>
|
||||||
</h3>
|
|
||||||
<editor
|
|
||||||
:is-edit-enabled="canWrite"
|
|
||||||
:upload-callback="attachmentUpload"
|
|
||||||
:upload-enabled="true"
|
|
||||||
@change="saveTask"
|
|
||||||
placeholder="Click here to enter a description..."
|
|
||||||
v-model="task.description"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Attachments -->
|
<!-- Attachments -->
|
||||||
|
@ -346,7 +322,8 @@
|
||||||
|
|
||||||
<!-- Created / Updated [by] -->
|
<!-- Created / Updated [by] -->
|
||||||
<p class="created">
|
<p class="created">
|
||||||
Created <span v-tooltip="formatDate(task.created)">{{ formatDateSince(task.created) }}</span> by {{ task.createdBy.getDisplayName() }}
|
Created <span v-tooltip="formatDate(task.created)">{{ formatDateSince(task.created) }}</span>
|
||||||
|
by {{ task.createdBy.getDisplayName() }}
|
||||||
<template v-if="+new Date(task.created) !== +new Date(task.updated)">
|
<template v-if="+new Date(task.created) !== +new Date(task.updated)">
|
||||||
<br/>
|
<br/>
|
||||||
<!-- Computed properties to show the actual date every time it gets updated -->
|
<!-- Computed properties to show the actual date every time it gets updated -->
|
||||||
|
@ -393,10 +370,10 @@ import Reminders from '../../components/tasks/partials/reminders'
|
||||||
import Comments from '../../components/tasks/partials/comments'
|
import Comments from '../../components/tasks/partials/comments'
|
||||||
import router from '../../router'
|
import router from '../../router'
|
||||||
import ListSearch from '../../components/tasks/partials/listSearch'
|
import ListSearch from '../../components/tasks/partials/listSearch'
|
||||||
|
import description from '@/components/tasks/partials/description'
|
||||||
import ColorPicker from '../../components/input/colorPicker'
|
import ColorPicker from '../../components/input/colorPicker'
|
||||||
import attachmentUpload from '../../components/tasks/mixins/attachmentUpload'
|
import attachmentUpload from '../../components/tasks/mixins/attachmentUpload'
|
||||||
import LoadingComponent from '../../components/misc/loading'
|
import heading from '@/components/tasks/partials/heading'
|
||||||
import ErrorComponent from '../../components/misc/error'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TaskDetailView',
|
name: 'TaskDetailView',
|
||||||
|
@ -413,12 +390,8 @@ export default {
|
||||||
PrioritySelect,
|
PrioritySelect,
|
||||||
Comments,
|
Comments,
|
||||||
flatPickr,
|
flatPickr,
|
||||||
editor: () => ({
|
description,
|
||||||
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'),
|
heading,
|
||||||
loading: LoadingComponent,
|
|
||||||
error: ErrorComponent,
|
|
||||||
timeout: 60000,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
attachmentUpload,
|
attachmentUpload,
|
||||||
|
@ -441,10 +414,12 @@ export default {
|
||||||
taskColor: '',
|
taskColor: '',
|
||||||
|
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
taskTitle: '',
|
|
||||||
descriptionChanged: false,
|
descriptionChanged: false,
|
||||||
listViewName: 'list.list',
|
listViewName: 'list.list',
|
||||||
|
|
||||||
|
descriptionSaving: false,
|
||||||
|
descriptionRecentlySaved: false,
|
||||||
|
|
||||||
priorities: priorites,
|
priorities: priorites,
|
||||||
flatPickerConfig: {
|
flatPickerConfig: {
|
||||||
altFormat: 'j M Y H:i',
|
altFormat: 'j M Y H:i',
|
||||||
|
@ -519,7 +494,6 @@ export default {
|
||||||
.then(r => {
|
.then(r => {
|
||||||
this.$set(this, 'task', r)
|
this.$set(this, 'task', r)
|
||||||
this.$store.commit('attachments/set', r.attachments)
|
this.$store.commit('attachments/set', r.attachments)
|
||||||
this.taskTitle = this.task.title
|
|
||||||
this.taskColor = this.task.hexColor
|
this.taskColor = this.task.hexColor
|
||||||
this.setActiveFields()
|
this.setActiveFields()
|
||||||
this.setTitle(this.task.title)
|
this.setTitle(this.task.title)
|
||||||
|
@ -547,23 +521,7 @@ export default {
|
||||||
this.activeFields.attachments = this.task.attachments.length > 0
|
this.activeFields.attachments = this.task.attachments.length > 0
|
||||||
this.activeFields.relatedTasks = Object.keys(this.task.relatedTasks).length > 0
|
this.activeFields.relatedTasks = Object.keys(this.task.relatedTasks).length > 0
|
||||||
},
|
},
|
||||||
saveTaskOnChange() {
|
saveTask(showNotification = true, undoCallback = null) {
|
||||||
this.$refs.taskTitle.spellcheck = false
|
|
||||||
|
|
||||||
// Pull the task title from the contenteditable
|
|
||||||
let taskTitle = this.$refs.taskTitle.textContent
|
|
||||||
this.task.title = taskTitle
|
|
||||||
|
|
||||||
// We only want to save if the title was actually change.
|
|
||||||
// Because the contenteditable does not have a change event,
|
|
||||||
// we're building it ourselves and only calling saveTask()
|
|
||||||
// if the task title changed.
|
|
||||||
if (this.task.title !== this.taskTitle) {
|
|
||||||
this.saveTask()
|
|
||||||
this.taskTitle = taskTitle
|
|
||||||
}
|
|
||||||
},
|
|
||||||
saveTask(undoCallback = null) {
|
|
||||||
|
|
||||||
if (!this.canWrite) {
|
if (!this.canWrite) {
|
||||||
return
|
return
|
||||||
|
@ -584,15 +542,20 @@ export default {
|
||||||
this.$store.dispatch('tasks/update', this.task)
|
this.$store.dispatch('tasks/update', this.task)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
this.$set(this, 'task', r)
|
this.$set(this, 'task', r)
|
||||||
|
this.setActiveFields()
|
||||||
|
|
||||||
|
if (!showNotification) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let actions = []
|
let actions = []
|
||||||
if (undoCallback !== null) {
|
if (undoCallback !== null) {
|
||||||
actions = [{
|
actions = [{
|
||||||
title: 'Undo',
|
title: 'Undo',
|
||||||
callback: undoCallback,
|
callback: undoCallback,
|
||||||
}]
|
}]
|
||||||
this.success({message: 'The task was saved successfully.'}, this, actions)
|
|
||||||
}
|
}
|
||||||
this.setActiveFields()
|
this.success({message: 'The task was saved successfully.'}, this, actions)
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
this.error(e, this)
|
this.error(e, this)
|
||||||
|
@ -610,7 +573,7 @@ export default {
|
||||||
deleteTask() {
|
deleteTask() {
|
||||||
this.$store.dispatch('tasks/delete', this.task)
|
this.$store.dispatch('tasks/delete', this.task)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.success({message: 'The task been deleted successfully.'}, this)
|
this.success({message: 'The task has been deleted successfully.'}, this)
|
||||||
router.back()
|
router.back()
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
|
@ -619,7 +582,7 @@ export default {
|
||||||
},
|
},
|
||||||
toggleTaskDone() {
|
toggleTaskDone() {
|
||||||
this.task.done = !this.task.done
|
this.task.done = !this.task.done
|
||||||
this.saveTask(() => this.toggleTaskDone())
|
this.saveTask(true, () => this.toggleTaskDone())
|
||||||
},
|
},
|
||||||
setDescriptionChanged(e) {
|
setDescriptionChanged(e) {
|
||||||
if (e.key === 'Enter' || e.key === 'Control') {
|
if (e.key === 'Enter' || e.key === 'Control') {
|
||||||
|
|
Loading…
Reference in a new issue