Add easymde & markdown preview for editing descriptions and comments (#183)
Make sure no text from previous mounts is left in the editor text field Make preview not the default when rendering descrition settings Add option to show editor by default while still having the option to show preview Add option to show editor by default while still having the option to show preview Use editor component for edit labels Use editor component for edit team Use editor component for edit namespace Use editor component for edit list Use editor component for edit task Make sure we find all checkboxes Fix checking wrong checkbox Make finding and replacing checkboxes in a function actually work Add upading text with checked checkboxes Lazy load editor Remove preview since we have a better one Make easymde smaller by default Add image upload from comments Rename easymde component to editor Only show preview button if editing is currently active Make editor tabs look better when commenting Make comments meta look better Don't try to update if the value was initially set Use editor to render and edit comments Make preview optional Make tabs look better Don't switch to preview after editing Centralize attachment state Render markdown by default Fix title being "null" Fix loading attachment images Add standalone preview Fix callback url Add onsuccess callback Add file upload Fix date parsing once and for all Add more props for upload and such Fix editor border color Fix changing text after mounting Add link to guide Fix sizing of icons Add timeout for changes Add all easymde icons Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/183
This commit is contained in:
@ -3,17 +3,28 @@
|
||||
<div class="field">
|
||||
<label class="label" for="tasktext">Task Text</label>
|
||||
<div class="control">
|
||||
<input v-focus :class="{ 'disabled': taskService.loading}" :disabled="taskService.loading" class="input"
|
||||
type="text" id="tasktext" placeholder="The task text is here..." v-model="taskEditTask.title" @change="editTaskSubmit()">
|
||||
<input
|
||||
v-focus
|
||||
:class="{ 'disabled': taskService.loading}"
|
||||
:disabled="taskService.loading"
|
||||
class="input"
|
||||
type="text"
|
||||
id="tasktext"
|
||||
placeholder="The task text is here..."
|
||||
v-model="taskEditTask.title"
|
||||
@change="editTaskSubmit()"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="taskdescription">Description</label>
|
||||
<div class="control">
|
||||
<textarea :class="{ 'disabled': taskService.loading}" :disabled="taskService.loading" class="textarea"
|
||||
placeholder="The tasks description goes here..." id="taskdescription"
|
||||
v-model="taskEditTask.description" @change="editTaskSubmit()">
|
||||
</textarea>
|
||||
<editor
|
||||
placeholder="The tasks description goes here..."
|
||||
id="taskdescription"
|
||||
v-model="taskEditTask.description"
|
||||
:preview-is-default="false"
|
||||
v-if="editorActive"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -106,7 +117,10 @@
|
||||
|
||||
<div class="field has-addons">
|
||||
<div class="control is-expanded">
|
||||
<edit-assignees :task-id="taskEditTask.id" :list-id="taskEditTask.listId" :initial-assignees="taskEditTask.assignees"/>
|
||||
<edit-assignees
|
||||
:task-id="taskEditTask.id"
|
||||
:list-id="taskEditTask.listId"
|
||||
:initial-assignees="taskEditTask.assignees"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -118,10 +132,10 @@
|
||||
</div>
|
||||
|
||||
<related-tasks
|
||||
class="is-narrow"
|
||||
:task-id="task.id"
|
||||
:list-id="task.listId"
|
||||
:initial-related-tasks="task.relatedTasks"
|
||||
class="is-narrow"
|
||||
:task-id="task.id"
|
||||
:list-id="task.listId"
|
||||
:initial-related-tasks="task.relatedTasks"
|
||||
/>
|
||||
|
||||
<button type="submit" class="button is-success is-fullwidth" :class="{ 'is-loading': taskService.loading}">
|
||||
@ -158,6 +172,7 @@
|
||||
|
||||
priorities: priorities,
|
||||
list: {},
|
||||
editorActive: false,
|
||||
newTask: TaskModel,
|
||||
isTaskEdit: false,
|
||||
taskEditTask: TaskModel,
|
||||
@ -181,6 +196,7 @@
|
||||
PercentDoneSelect,
|
||||
PrioritySelect,
|
||||
flatPickr,
|
||||
editor: () => import(/* webpackPrefetch: true */ '../../components/input/editor'),
|
||||
},
|
||||
props: {
|
||||
task: {
|
||||
@ -206,6 +222,13 @@
|
||||
this.taskEditTask.dueDate = +new Date(this.task.dueDate) === 0 ? null : this.task.dueDate
|
||||
this.taskEditTask.startDate = +new Date(this.task.startDate) === 0 ? null : this.task.startDate
|
||||
this.taskEditTask.endDate = +new Date(this.task.endDate) === 0 ? null : this.task.endDate
|
||||
// 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)
|
||||
},
|
||||
editTaskSubmit() {
|
||||
this.taskService.update(this.taskEditTask)
|
||||
|
32
src/components/tasks/mixins/attachmentUpload.js
Normal file
32
src/components/tasks/mixins/attachmentUpload.js
Normal file
@ -0,0 +1,32 @@
|
||||
import AttachmentModel from '../../../models/attachment'
|
||||
import AttachmentService from '../../../services/attachment'
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
attachmentUpload(file, onSuccess) {
|
||||
const files = [file]
|
||||
|
||||
const attachmentService = new AttachmentService()
|
||||
|
||||
const attachmentModel = new AttachmentModel({taskId: this.taskId})
|
||||
attachmentService.create(attachmentModel, files)
|
||||
.then(r => {
|
||||
if (r.success !== null) {
|
||||
r.success.forEach(a => {
|
||||
this.$store.commit('attachments/add', a)
|
||||
this.$store.dispatch('tasks/addTaskAttachment', {taskId: this.taskId, attachment: a})
|
||||
onSuccess(`${window.API_URL}/tasks/${this.taskId}/attachments/${a.id}`)
|
||||
})
|
||||
}
|
||||
if (r.errors !== null) {
|
||||
r.errors.forEach(m => {
|
||||
this.error(m)
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
@ -79,6 +79,7 @@
|
||||
import AttachmentService from '../../../services/attachment'
|
||||
import AttachmentModel from '../../../models/attachment'
|
||||
import User from '../../misc/user'
|
||||
import {mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'attachments',
|
||||
@ -87,7 +88,6 @@
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
attachments: [],
|
||||
attachmentService: AttachmentService,
|
||||
showDropzone: false,
|
||||
|
||||
@ -106,8 +106,10 @@
|
||||
},
|
||||
created() {
|
||||
this.attachmentService = new AttachmentService()
|
||||
this.attachments = this.initialAttachments
|
||||
},
|
||||
computed: mapState({
|
||||
attachments: state => state.attachments.attachments
|
||||
}),
|
||||
mounted() {
|
||||
document.addEventListener('dragenter', e => {
|
||||
e.stopPropagation()
|
||||
@ -136,11 +138,6 @@
|
||||
this.showDropzone = false
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
initialAttachments(newVal) {
|
||||
this.attachments = newVal
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
downloadAttachment(attachment) {
|
||||
this.attachmentService.download(attachment)
|
||||
@ -158,7 +155,7 @@
|
||||
.then(r => {
|
||||
if(r.success !== null) {
|
||||
r.success.forEach(a => {
|
||||
this.attachments.push(a)
|
||||
this.$store.commit('attachments/add', a)
|
||||
this.$store.dispatch('tasks/addTaskAttachment', {taskId: this.taskId, attachment: a})
|
||||
})
|
||||
}
|
||||
@ -171,17 +168,11 @@
|
||||
.catch(e => {
|
||||
this.error(e, this)
|
||||
})
|
||||
|
||||
},
|
||||
deleteAttachment() {
|
||||
this.attachmentService.delete(this.attachmentToDelete)
|
||||
.then(r => {
|
||||
// Remove the file from the list
|
||||
for (const a in this.attachments) {
|
||||
if (this.attachments[a].id === this.attachmentToDelete.id) {
|
||||
this.attachments.splice(a, 1)
|
||||
}
|
||||
}
|
||||
this.$store.commit('attachments/removeById', this.attachmentToDelete.id)
|
||||
this.success(r, this)
|
||||
})
|
||||
.catch(e => {
|
||||
|
@ -7,33 +7,29 @@
|
||||
Comments
|
||||
</h1>
|
||||
<div class="comments">
|
||||
<progress class="progress is-small is-info" max="100" v-if="taskCommentService.loading">Loading comments...</progress>
|
||||
<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="" width="48" height="48"/>
|
||||
</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>
|
||||
<div class="comment-info">
|
||||
<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>
|
||||
<small v-if="+new Date(c.created) !== +new Date(c.updated)" v-tooltip="formatDate(c.updated)"> ·
|
||||
edited {{ formatDateSince(c.updated) }}</small>
|
||||
</div>
|
||||
<editor
|
||||
v-model="c.comment"
|
||||
:has-preview="true"
|
||||
@change="() => {toggleEdit(c);editComment()}"
|
||||
:upload-enabled="true"
|
||||
:upload-callback="attachmentUpload"
|
||||
/>
|
||||
<div class="comment-actions">
|
||||
<a @click="toggleDelete(c.id)">Remove</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -44,10 +40,21 @@
|
||||
<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>
|
||||
<editor
|
||||
placeholder="Add your comment..."
|
||||
:class="{'is-loading': taskCommentService.loading && !isCommentEdit}"
|
||||
v-model="newComment.comment"
|
||||
:has-preview="false"
|
||||
:upload-enabled="true"
|
||||
:upload-callback="attachmentUpload"
|
||||
v-if="editorActive"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
<button class="button is-primary" :class="{'is-loading': taskCommentService.loading && !isCommentEdit}" @click="addComment()" :disabled="newComment.comment === ''">Comment</button>
|
||||
<button class="button is-primary"
|
||||
:class="{'is-loading': taskCommentService.loading && !isCommentEdit}"
|
||||
@click="addComment()" :disabled="newComment.comment === ''">Comment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,9 +74,16 @@
|
||||
<script>
|
||||
import TaskCommentService from '../../../services/taskComment'
|
||||
import TaskCommentModel from '../../../models/taskComment'
|
||||
import attachmentUpload from '../mixins/attachmentUpload'
|
||||
|
||||
export default {
|
||||
name: 'comments',
|
||||
components: {
|
||||
editor: () => import(/* webpackPrefetch: true */ '../../input/editor'),
|
||||
},
|
||||
mixins: [
|
||||
attachmentUpload,
|
||||
],
|
||||
props: {
|
||||
taskId: {
|
||||
type: Number,
|
||||
@ -88,6 +102,7 @@
|
||||
|
||||
taskCommentService: TaskCommentService,
|
||||
newComment: TaskCommentModel,
|
||||
editorActive: true,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -124,6 +139,15 @@
|
||||
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.taskCommentService.create(this.newComment)
|
||||
.then(r => {
|
||||
this.comments.push(r)
|
||||
|
Reference in New Issue
Block a user