Task Comments #66

Merged
konrad merged 12 commits from feature/comments into master 2020-02-25 20:11:36 +00:00
9 changed files with 261 additions and 1 deletions

View File

@ -190,6 +190,9 @@
ref="relatedTasks"
/>
</div>
<!-- Comments -->
<comments :task-i-d="taskID"/>
</div>
<div class="column is-one-fifth action-buttons">
<a class="button is-outlined noshadow has-no-border" :class="{'is-success': !task.done}" @click="toggleTaskDone()">
@ -288,6 +291,7 @@
import RelatedTasks from './reusable/relatedTasks'
import RepeatAfter from './reusable/repeatAfter'
import Reminders from './reusable/reminders'
import Comments from './reusable/comments'
import router from '../../router'
export default {
@ -301,6 +305,7 @@
EditLabels,
PercentDoneSelect,
PrioritySelect,
Comments,
flatPickr,
},
data() {

View File

@ -0,0 +1,177 @@
<template>
<div class="content details has-top-border">
<h1>
<span class="icon is-grey">
<icon :icon="['far', 'comments']"/>
</span>
Comments
</h1>
<div class="comments">
<progress class="progress is-small is-info" max="100" v-if="taskCommentService.loading">Loading comments...</progress>
<div class="media comment" v-for="c in comments" :key="c.id">
<figure class="media-left">
<img class="image is-avatar" :src="c.author.getAvatarUrl(48)" alt="">
</figure>
<div class="media-content">
<div class="form" v-if="isCommentEdit && commentEdit.id === c.id">
<div class="field">
<textarea class="textarea" :class="{'is-loading': taskCommentService.loading}" placeholder="Add your comment..." v-model="commentEdit.comment" @keyup.ctrl.enter="editComment()"></textarea>
</div>
<div class="field">
<button class="button is-primary" :class="{'is-loading': taskCommentService.loading}" @click="editComment()" :disabled="commentEdit.comment === ''">Comment</button>
<a @click="() => isCommentEdit = false">Cancel</a>
</div>
</div>
<div class="content" v-else>
<strong>{{ c.author.username }}</strong>&nbsp;
<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)"> · edited {{ formatDateSince(c.updated) }}</small>
<br/>
<p>
{{c.comment}}
</p>
<div class="comment-actions">
<a @click="toggleEdit(c)">Edit</a>&nbsp;·&nbsp;
<a @click="toggleDelete(c.id)">Remove</a>
</div>
</div>
</div>
</div>
<div class="media comment">
<figure class="media-left">
<img class="image is-avatar" :src="user.infos.getAvatarUrl(48)" alt="">
</figure>
<div class="media-content">
<div class="form">
<div class="field">
<textarea class="textarea" :class="{'is-loading': taskCommentService.loading && !isCommentEdit}" placeholder="Add your comment..." v-model="newComment.comment" @keyup.ctrl.enter="addComment()"></textarea>
</div>
<div class="field">
<button class="button is-primary" :class="{'is-loading': taskCommentService.loading && !isCommentEdit}" @click="addComment()" :disabled="newComment.comment === ''">Comment</button>
</div>
</div>
</div>
</div>
</div>
<modal
v-if="showDeleteModal"
@close="showDeleteModal = false"
@submit="deleteComment()">
<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 auth from '../../../auth'
export default {
name: 'comments',
props: {
taskID: {
type: Number,
required: true,
}
},
data() {
return {
comments: [],
user: auth.user,
showDeleteModal: false,
commentToDelete: TaskCommentModel,
isCommentEdit: false,
commentEdit: TaskCommentModel,
taskCommentService: TaskCommentService,
newComment: TaskCommentModel,
}
},
created() {
this.taskCommentService = new TaskCommentService()
this.newComment = new TaskCommentModel({task_id: this.taskID})
this.commentEdit = new TaskCommentModel({task_id: this.taskID})
this.commentToDelete = new TaskCommentModel({task_id: this.taskID})
this.comments = []
},
mounted() {
this.loadComments()
},
methods: {
loadComments() {
this.taskCommentService.getAll({task_id: this.taskID})
.then(r => {
this.$set(this, 'comments', r)
})
.catch(e => {
this.error(e, this)
})
},
addComment() {
if (this.newComment.comment === '') {
return
}
this.taskCommentService.create(this.newComment)
.then(r => {
this.comments.push(r)
this.success({message: 'The comment was sucessfully added.'}, this)
this.newComment.comment = ''
})
.catch(e => {
this.error(e, this)
})
},
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.commentEdit.task_id = 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.success({message: 'The comment was successfully updated.'}, this)
})
.catch(e => {
this.error(e, this)
})
.finally(() => {
this.isCommentEdit = false
})
},
deleteComment() {
this.taskCommentService.delete(this.commentToDelete)
.then(r => {
for (const a in this.comments) {
if (this.comments[a].id === this.commentToDelete.id) {
this.comments.splice(a, 1)
}
}
this.success(r, this)
})
.catch(e => {
this.error(e, this)
})
.finally(() => {
this.showDeleteModal = false
})
},
},
}
</script>

View File

@ -64,6 +64,7 @@ import { faClock } from '@fortawesome/free-regular-svg-icons'
import { faHistory } from '@fortawesome/free-solid-svg-icons'
import { faSearch } from '@fortawesome/free-solid-svg-icons'
import { faCheckDouble } from '@fortawesome/free-solid-svg-icons'
import { faComments } from '@fortawesome/free-regular-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
library.add(faSignOutAlt)
@ -102,6 +103,7 @@ library.add(faClock)
library.add(faHistory)
library.add(faSearch)
library.add(faCheckDouble)
library.add(faComments)
Vue.component('icon', FontAwesomeIcon)

22
src/models/taskComment.js Normal file
View File

@ -0,0 +1,22 @@
import AbstractModel from './abstractModel'
import UserModel from './user'
export default class TaskCommentModel extends AbstractModel {
constructor(data) {
super(data)
this.author = new UserModel(this.author)
this.created = new Date(this.created)
this.updated = new Date(this.updated)
}
defaults() {
return {
id: 0,
task_id: 0,
comment: '',
author: UserModel,
created: null,
update: null,
}
}
}

View File

@ -19,6 +19,7 @@ export default class UserModel extends AbstractModel {
}
getAvatarUrl(size = 50) {
return `https://www.gravatar.com/avatar/${this.avatar}?s=${size}&d=mp`
const avatarUrl = this.avatar !== '' ? this.avatar : this.avatarUrl
return `https://www.gravatar.com/avatar/${avatarUrl}?s=${size}&d=mp`
}
}

View File

@ -0,0 +1,25 @@
import AbstractService from './abstractService'
import TaskCommentModel from '../models/taskComment'
import moment from 'moment'
export default class TaskCommentService extends AbstractService {
constructor() {
super({
create: '/tasks/{task_id}/comments',
getAll: '/tasks/{task_id}/comments',
get: '/tasks/{task_id}/comments/{id}',
update: '/tasks/{task_id}/comments/{id}',
delete: '/tasks/{task_id}/comments/{id}',
})
}
processModel(model) {
model.created = moment(model.created).toISOString()
model.updated = moment(model.updated).toISOString()
return model
}
modelFactory(data) {
return new TaskCommentModel(data)
}
}

View File

@ -12,3 +12,4 @@
@import 'tasks';
@import 'teams';
@import 'migrator';
@import 'comments';

View File

@ -0,0 +1,19 @@
.media.comment{
align-items: center;
.media-left {
margin: 0 1em;
}
.comment-actions {
font-size: .8em;
&, a {
color: $grey;
}
a:hover {
text-decoration: underline;
}
}
}

View File

@ -30,3 +30,11 @@ h1,h2,h3,h4,h5,h6{
.has-no-border{
border: none !important;
}
.has-rounded-corners {
border-radius: $radius;
}
.image.is-avatar{
border-radius: 100%;
}