148cc1dcca
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>
266 lines
7.2 KiB
Vue
266 lines
7.2 KiB
Vue
<template>
|
|
<div :class="{'has-top-border': canWrite || comments.length > 0}" class="content details">
|
|
<h1 v-if="canWrite || comments.length > 0">
|
|
<span class="icon is-grey">
|
|
<icon :icon="['far', 'comments']"/>
|
|
</span>
|
|
Comments
|
|
</h1>
|
|
<div class="comments">
|
|
<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...
|
|
</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"/>
|
|
</figure>
|
|
<div class="media-content">
|
|
<div class="comment-info">
|
|
<img :src="c.author.getAvatarUrl(20)" alt="" class="image is-avatar" height="20" width="20"/>
|
|
<strong>{{ c.author.getDisplayName() }}</strong>
|
|
<span v-tooltip="formatDate(c.created)">{{ formatDateSince(c.created) }}</span>
|
|
<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"
|
|
:is-edit-enabled="canWrite"
|
|
:upload-callback="attachmentUpload"
|
|
:upload-enabled="true"
|
|
@change="() => {toggleEdit(c);editComment()}"
|
|
v-model="c.comment"
|
|
:has-edit-bottom="true"
|
|
:bottom-actions="actions[c.id]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="media comment" v-if="canWrite">
|
|
<figure class="media-left is-hidden-mobile">
|
|
<img :src="userAvatar" alt="" class="image is-avatar" height="48" width="48"/>
|
|
</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}"
|
|
:has-preview="false"
|
|
:upload-callback="attachmentUpload"
|
|
:upload-enabled="true"
|
|
placeholder="Add your comment..."
|
|
v-if="editorActive"
|
|
v-model="newComment.comment"
|
|
/>
|
|
</div>
|
|
<div class="field">
|
|
<button :class="{'is-loading': taskCommentService.loading && !isCommentEdit}"
|
|
:disabled="newComment.comment === ''"
|
|
@click="addComment()" class="button is-primary">Comment
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<modal
|
|
@close="showDeleteModal = false"
|
|
@submit="deleteComment()"
|
|
v-if="showDeleteModal">
|
|
<span slot="header">Delete this comment</span>
|
|
<p slot="text">Are you sure you want to delete this comment?
|
|
<br/>This <b>CANNOT BE UNDONE!</b></p>
|
|
</modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import TaskCommentService from '../../../services/taskComment'
|
|
import TaskCommentModel from '../../../models/taskComment'
|
|
import attachmentUpload from '../mixins/attachmentUpload'
|
|
import LoadingComponent from '../../misc/loading'
|
|
import ErrorComponent from '../../misc/error'
|
|
|
|
export default {
|
|
name: 'comments',
|
|
components: {
|
|
editor: () => ({
|
|
component: import(/* webpackChunkName: "editor" */ '../../input/editor'),
|
|
loading: LoadingComponent,
|
|
error: ErrorComponent,
|
|
timeout: 60000,
|
|
}),
|
|
},
|
|
mixins: [
|
|
attachmentUpload,
|
|
],
|
|
props: {
|
|
taskId: {
|
|
type: Number,
|
|
required: true,
|
|
},
|
|
canWrite: {
|
|
default: true,
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
comments: [],
|
|
|
|
showDeleteModal: false,
|
|
commentToDelete: TaskCommentModel,
|
|
|
|
isCommentEdit: false,
|
|
commentEdit: TaskCommentModel,
|
|
|
|
taskCommentService: TaskCommentService,
|
|
newComment: TaskCommentModel,
|
|
editorActive: true,
|
|
actions: {},
|
|
|
|
saved: null,
|
|
saving: null,
|
|
creating: false,
|
|
}
|
|
},
|
|
created() {
|
|
this.taskCommentService = new TaskCommentService()
|
|
this.newComment = new TaskCommentModel({taskId: this.taskId})
|
|
this.commentEdit = new TaskCommentModel({taskId: this.taskId})
|
|
this.commentToDelete = new TaskCommentModel({taskId: this.taskId})
|
|
this.comments = []
|
|
},
|
|
mounted() {
|
|
this.loadComments()
|
|
},
|
|
watch: {
|
|
taskId() {
|
|
this.loadComments()
|
|
},
|
|
canWrite() {
|
|
this.makeActions()
|
|
},
|
|
},
|
|
computed: {
|
|
userAvatar() {
|
|
return this.$store.state.auth.info.getAvatarUrl(48)
|
|
},
|
|
},
|
|
methods: {
|
|
loadComments() {
|
|
this.taskCommentService.getAll({taskId: this.taskId})
|
|
.then(r => {
|
|
this.$set(this, 'comments', r)
|
|
this.makeActions()
|
|
})
|
|
.catch(e => {
|
|
this.error(e, this)
|
|
})
|
|
},
|
|
addComment() {
|
|
if (this.newComment.comment === '') {
|
|
return
|
|
}
|
|
|
|
// This makes the editor trigger its mounted function again which makes it forget every input
|
|
// it currently has in its textarea. This is a counter-hack to a hack inside of vue-easymde
|
|
// which made it impossible to detect change from the outside. Therefore the component would
|
|
// not update if new content from the outside was made available.
|
|
// 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
|
|
this.commentEdit = comment
|
|
},
|
|
toggleDelete(commentId) {
|
|
this.showDeleteModal = !this.showDeleteModal
|
|
this.commentToDelete.id = commentId
|
|
},
|
|
editComment() {
|
|
if (this.commentEdit.comment === '') {
|
|
return
|
|
}
|
|
|
|
this.saving = this.commentEdit.id
|
|
|
|
this.commentEdit.taskId = this.taskId
|
|
this.taskCommentService.update(this.commentEdit)
|
|
.then(r => {
|
|
for (const c in this.comments) {
|
|
if (this.comments[c].id === this.commentEdit.id) {
|
|
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() {
|
|
this.taskCommentService.delete(this.commentToDelete)
|
|
.then(() => {
|
|
for (const a in this.comments) {
|
|
if (this.comments[a].id === this.commentToDelete.id) {
|
|
this.comments.splice(a, 1)
|
|
}
|
|
}
|
|
})
|
|
.catch(e => {
|
|
this.error(e, this)
|
|
})
|
|
.finally(() => {
|
|
this.showDeleteModal = false
|
|
})
|
|
},
|
|
makeActions() {
|
|
if (this.canWrite) {
|
|
this.comments.forEach(c => {
|
|
this.$set(this.actions, c.id, [{
|
|
action: () => this.toggleDelete(c.id),
|
|
title: 'Remove',
|
|
}])
|
|
})
|
|
}
|
|
}
|
|
},
|
|
}
|
|
</script>
|