1
0

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:
konrad
2020-11-22 16:32:35 +00:00
parent b9eeec0125
commit 148cc1dcca
11 changed files with 456 additions and 223 deletions

View File

@ -7,9 +7,10 @@
Comments
</h1>
<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...
</progress>
</span>
<div :key="c.id" class="media comment" v-for="c in comments">
<figure class="media-left is-hidden-mobile">
<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)">
· edited {{ formatDateSince(c.updated) }}
</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>
<editor
:has-preview="true"
@ -41,6 +51,12 @@
</figure>
<div class="media-content">
<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">
<editor
:class="{'is-loading': taskCommentService.loading && !isCommentEdit}"
@ -116,6 +132,10 @@ export default {
newComment: TaskCommentModel,
editorActive: true,
actions: {},
saved: null,
saving: null,
creating: false,
}
},
created() {
@ -164,15 +184,20 @@ export default {
// See https://github.com/NikulinIlya/vue-easymde/issues/3
this.editorActive = false
this.$nextTick(() => this.editorActive = true)
this.creating = true
this.taskCommentService.create(this.newComment)
.then(r => {
this.comments.push(r)
this.newComment.comment = ''
this.success({message: 'The comment was added successfully.'}, this)
})
.catch(e => {
this.error(e, this)
})
.finally(() => {
this.creating = false
})
},
toggleEdit(comment) {
this.isCommentEdit = !this.isCommentEdit
@ -186,6 +211,9 @@ export default {
if (this.commentEdit.comment === '') {
return
}
this.saving = this.commentEdit.id
this.commentEdit.taskId = this.taskId
this.taskCommentService.update(this.commentEdit)
.then(r => {
@ -194,12 +222,17 @@ export default {
this.$set(this.comments, c, r)
}
}
this.saved = this.commentEdit.id
setTimeout(() => {
this.saved = null
}, 2000)
})
.catch(e => {
this.error(e, this)
})
.finally(() => {
this.isCommentEdit = false
this.saving = null
})
},
deleteComment() {

View 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>

View File

@ -97,6 +97,7 @@ export default {
this.$store.dispatch('tasks/addAssignee', {user: user, taskId: this.taskId})
.then(() => {
this.$emit('input', this.assignees)
this.success({message: 'The user has been assigned successfully.'}, this)
})
.catch(e => {
this.error(e, this)
@ -111,6 +112,7 @@ export default {
this.assignees.splice(a, 1)
}
}
this.success({message: 'The user has been unassinged successfully.'}, this)
})
.catch(e => {
this.error(e, this)

View File

@ -13,7 +13,7 @@
:showNoOptions="false"
:taggable="true"
@search-change="findLabel"
@select="addLabel"
@select="label => addLabel(label)"
@tag="createAndAddLabel"
label="title"
placeholder="Type to add a new label..."
@ -121,10 +121,13 @@ export default {
clearAllLabels() {
this.$set(this, 'foundLabels', [])
},
addLabel(label) {
addLabel(label, showNotification = true) {
this.$store.dispatch('tasks/addLabel', {label: label, taskId: this.taskId})
.then(() => {
this.$emit('input', this.labels)
if (showNotification) {
this.success({message: 'The label has been added successfully.'}, this)
}
})
.catch(e => {
this.error(e, this)
@ -140,6 +143,7 @@ export default {
}
}
this.$emit('input', this.labels)
this.success({message: 'The label has been removed successfully.'}, this)
})
.catch(e => {
this.error(e, this)
@ -149,8 +153,9 @@ export default {
let newLabel = new LabelModel({title: title})
this.labelService.create(newLabel)
.then(r => {
this.addLabel(r)
this.addLabel(r, false)
this.labels.push(r)
this.success({message: 'The label has been created successfully.'}, this)
})
.catch(e => {
this.error(e, this)

View 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>

View File

@ -1,7 +1,18 @@
<template>
<div class="task-relations">
<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">
<multiselect
:internal-search="true"
@ -112,6 +123,7 @@ export default {
taskRelationService: TaskRelationService,
showDeleteModal: false,
relationToDelete: {},
saved: false,
}
},
components: {
@ -188,6 +200,10 @@ export default {
}
this.relatedTasks[this.newTaskRelationKind].push(this.newTaskRelationTask)
this.newTaskRelationTask = new TaskModel()
this.saved = true
setTimeout(() => {
this.saved = false
}, 2000)
})
.catch(e => {
this.error(e, this)
@ -208,6 +224,10 @@ export default {
}
}
})
this.saved = true
setTimeout(() => {
this.saved = false
}, 2000)
})
.catch(e => {
this.error(e, this)