Task Comments #66
|
@ -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() {
|
||||
|
|
177
src/components/tasks/reusable/comments.vue
Normal file
177
src/components/tasks/reusable/comments.vue
Normal 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>
|
||||
<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> ·
|
||||
<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>
|
|
@ -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
22
src/models/taskComment.js
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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`
|
||||
}
|
||||
}
|
25
src/services/taskComment.js
Normal file
25
src/services/taskComment.js
Normal 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)
|
||||
}
|
||||
}
|
|
@ -12,3 +12,4 @@
|
|||
@import 'tasks';
|
||||
@import 'teams';
|
||||
@import 'migrator';
|
||||
@import 'comments';
|
||||
|
|
19
src/styles/components/comments.scss
Normal file
19
src/styles/components/comments.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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%;
|
||||
}
|
||||
|
|
Reference in New Issue
Block a user