Better save messages for tasks #307

Merged
konrad merged 9 commits from feature/save-messages into master 2020-11-22 16:32:36 +00:00
11 changed files with 456 additions and 223 deletions

View File

@ -1,5 +1,5 @@
<template>
<notifications position="bottom left">
<notifications position="bottom left" :max="2">
<template slot="body" slot-scope="props">
<div :class="['vue-notification-template', 'vue-notification', props.item.type]" @click="close(props)">
<div

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)

View File

@ -3,12 +3,15 @@ import TaskAssigneeService from '../../services/taskAssignee'
import TaskAssigneeModel from '../../models/taskAssignee'
import LabelTaskModel from '../../models/labelTask'
import LabelTaskService from '../../services/labelTask'
import {setLoading} from '@/store/helper'
export default {
namespaced: true,
state: () => ({}),
actions: {
update(ctx, task) {
const cancel = setLoading(ctx)
const taskService = new TaskService()
return taskService.update(task)
.then(t => {
@ -18,6 +21,9 @@ export default {
.catch(e => {
return Promise.reject(e)
})
.finally(() => {
cancel()
})
},
delete(ctx, task) {
const taskService = new TaskService()

View File

@ -44,3 +44,7 @@
.media-content {
width: calc(100% - 48px - 2em);
}
.content h3 .is-small {
font-size: 1rem;
}

View File

@ -1,104 +1,105 @@
<template>
<div :class="{ 'is-loading': loading}" class="kanban loader-container">
<div :key="`bucket${bucket.id}`" class="bucket" v-for="bucket in buckets">
<div class="bucket-header">
<h2
:ref="`bucket${bucket.id}title`"
@focusout="() => saveBucketTitle(bucket.id)"
@keyup.enter="() => saveBucketTitle(bucket.id)"
class="title input"
contenteditable="true"
spellcheck="false">{{ bucket.title }}</h2>
<span
:class="{'is-max': bucket.tasks.length >= bucket.limit}"
class="limit"
v-if="bucket.limit > 0">
<div>
<div :class="{ 'is-loading': loading}" class="kanban loader-container">
<div :key="`bucket${bucket.id}`" class="bucket" v-for="bucket in buckets">
<div class="bucket-header">
<h2
:ref="`bucket${bucket.id}title`"
@focusout="() => saveBucketTitle(bucket.id)"
@keyup.enter="() => saveBucketTitle(bucket.id)"
class="title input"
contenteditable="true"
spellcheck="false">{{ bucket.title }}</h2>
<span
:class="{'is-max': bucket.tasks.length >= bucket.limit}"
class="limit"
v-if="bucket.limit > 0">
{{ bucket.tasks.length }}/{{ bucket.limit }}
</span>
<div
:class="{ 'is-active': bucketOptionsDropDownActive[bucket.id] }"
class="dropdown is-right options"
v-if="canWrite"
>
<div @click.stop="toggleBucketDropdown(bucket.id)" class="dropdown-trigger">
<div
:class="{ 'is-active': bucketOptionsDropDownActive[bucket.id] }"
class="dropdown is-right options"
v-if="canWrite"
>
<div @click.stop="toggleBucketDropdown(bucket.id)" class="dropdown-trigger">
<span class="icon">
<icon icon="ellipsis-v"/>
</span>
</div>
<div class="dropdown-menu" role="menu">
<div class="dropdown-content">
<a
@click.stop="showSetLimitInput = true"
class="dropdown-item"
>
<div class="field has-addons" v-if="showSetLimitInput">
<div class="control">
<input
@change="() => updateBucket(bucket)"
@keyup.enter="() => updateBucket(bucket)"
class="input"
type="number"
v-focus.always
v-model="bucket.limit"
/>
</div>
<div class="control">
<a class="button is-primary has-no-shadow">
</div>
<div class="dropdown-menu" role="menu">
<div class="dropdown-content">
<a
@click.stop="showSetLimitInput = true"
class="dropdown-item"
>
<div class="field has-addons" v-if="showSetLimitInput">
<div class="control">
<input
@change="() => updateBucket(bucket)"
@keyup.enter="() => updateBucket(bucket)"
class="input"
type="number"
v-focus.always
v-model="bucket.limit"
/>
</div>
<div class="control">
<a class="button is-primary has-no-shadow">
<span class="icon">
<icon :icon="['far', 'save']"/>
</span>
</a>
</a>
</div>
</div>
</div>
<template v-else>
Limit: {{ bucket.limit > 0 ? bucket.limit : 'Not set' }}
</template>
</a>
<a
:class="{'is-disabled': buckets.length <= 1}"
@click="() => deleteBucketModal(bucket.id)"
class="dropdown-item has-text-danger"
v-tooltip="buckets.length <= 1 ? 'You cannot remove the last bucket.' : ''"
>
<span class="icon is-small"><icon icon="trash-alt"/></span>
Delete
</a>
<template v-else>
Limit: {{ bucket.limit > 0 ? bucket.limit : 'Not set' }}
</template>
</a>
<a
:class="{'is-disabled': buckets.length <= 1}"
@click="() => deleteBucketModal(bucket.id)"
class="dropdown-item has-text-danger"
v-tooltip="buckets.length <= 1 ? 'You cannot remove the last bucket.' : ''"
>
<span class="icon is-small"><icon icon="trash-alt"/></span>
Delete
</a>
</div>
</div>
</div>
</div>
</div>
<div :ref="`tasks-container${bucket.id}`" class="tasks">
<!-- Make the component either a div or a draggable component based on the user rights -->
<component
:animation-duration="150"
:drop-placeholder="dropPlaceholderOptions"
:get-child-payload="getTaskPayload(bucket.id)"
:is="canWrite ? 'Container' : 'div'"
:should-accept-drop="() => shouldAcceptDrop(bucket)"
@drop="e => onDrop(bucket.id, e)"
drag-class="ghost-task"
drag-class-drop="ghost-task-drop"
drag-handle-selector=".task.draggable"
group-name="buckets"
>
<div :ref="`tasks-container${bucket.id}`" class="tasks">
<!-- Make the component either a div or a draggable component based on the user rights -->
<component
:is="canWrite ? 'Draggable' : 'div'"
:key="`bucket${bucket.id}-task${task.id}`"
v-for="task in bucket.tasks"
:animation-duration="150"
:drop-placeholder="dropPlaceholderOptions"
:get-child-payload="getTaskPayload(bucket.id)"
:is="canWrite ? 'Container' : 'div'"
:should-accept-drop="() => shouldAcceptDrop(bucket)"
@drop="e => onDrop(bucket.id, e)"
drag-class="ghost-task"
drag-class-drop="ghost-task-drop"
drag-handle-selector=".task.draggable"
group-name="buckets"
>
<div
:class="{
<!-- Make the component either a div or a draggable component based on the user rights -->
<component
:is="canWrite ? 'Draggable' : 'div'"
:key="`bucket${bucket.id}-task${task.id}`"
v-for="task in bucket.tasks"
>
<div
:class="{
'is-loading': taskService.loading && taskUpdating[task.id],
'draggable': !taskService.loading || !taskUpdating[task.id],
'has-light-text': !colorIsDark(task.hexColor) && task.hexColor !== `#${task.defaultColor}` && task.hexColor !== task.defaultColor,
}"
:style="{'background-color': task.hexColor !== '#' && task.hexColor !== `#${task.defaultColor}` ? task.hexColor : false}"
@click.ctrl="() => markTaskAsDone(task)"
@click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })"
@click.meta="() => markTaskAsDone(task)"
class="task loader-container draggable"
>
:style="{'background-color': task.hexColor !== '#' && task.hexColor !== `#${task.defaultColor}` ? task.hexColor : false}"
@click.ctrl="() => markTaskAsDone(task)"
@click.exact="() => $router.push({ name: 'task.kanban.detail', params: { id: task.id } })"
@click.meta="() => markTaskAsDone(task)"
class="task loader-container draggable"
>
<span class="task-id">
<span class="is-done" v-if="task.done">Done</span>
<template v-if="task.identifier === ''">
@ -108,11 +109,11 @@
{{ task.identifier }}
</template>
</span>
<span
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="due-date"
v-if="task.dueDate > 0"
v-tooltip="formatDate(task.dueDate)">
<span
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="due-date"
v-if="task.dueDate > 0"
v-tooltip="formatDate(task.dueDate)">
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
@ -120,22 +121,22 @@
{{ formatDateSince(task.dueDate) }}
</span>
</span>
<h3>{{ task.title }}</h3>
<labels :labels="task.labels"/>
<div class="footer">
<div class="items">
<priority-label :priority="task.priority" class="priority-label"/>
<div class="assignees" v-if="task.assignees.length > 0">
<user
:avatar-size="24"
:key="task.id + 'assignee' + u.id"
:show-username="false"
:user="u"
v-for="u in task.assignees"
/>
<h3>{{ task.title }}</h3>
<labels :labels="task.labels"/>
<div class="footer">
<div class="items">
<priority-label :priority="task.priority" class="priority-label"/>
<div class="assignees" v-if="task.assignees.length > 0">
<user
:avatar-size="24"
:key="task.id + 'assignee' + u.id"
:show-username="false"
:user="u"
v-for="u in task.assignees"
/>
</div>
</div>
</div>
<div>
<div>
<span class="icon" v-if="task.attachments.length > 0">
<svg fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect fill="none" rx="0" ry="0"></rect>
@ -145,74 +146,75 @@
fill-rule="evenodd"></path>
</svg>
</span>
</div>
</div>
</div>
</div>
</component>
</component>
</component>
</div>
<div class="bucket-footer" v-if="canWrite">
<div class="field" v-if="showNewTaskInput[bucket.id]">
<div class="control">
<input
:class="{'is-loading': taskService.loading}"
:disabled="taskService.loading"
@focusout="toggleShowNewTaskInput(bucket.id)"
@keyup.enter="addTaskToBucket(bucket.id)"
@keyup.esc="toggleShowNewTaskInput(bucket.id)"
class="input"
placeholder="Enter the new task text..."
type="text"
v-focus.always
v-model="newTaskText"
/>
</div>
<p class="help is-danger" v-if="newTaskError[bucket.id] && newTaskText === ''">
Please specify a title.
</p>
</div>
<a
@click="toggleShowNewTaskInput(bucket.id)"
class="button noshadow is-transparent is-fullwidth has-text-centered"
v-if="!showNewTaskInput[bucket.id]">
<div class="bucket-footer" v-if="canWrite">
<div class="field" v-if="showNewTaskInput[bucket.id]">
<div class="control">
<input
:class="{'is-loading': taskService.loading}"
:disabled="taskService.loading"
@focusout="toggleShowNewTaskInput(bucket.id)"
@keyup.enter="addTaskToBucket(bucket.id)"
@keyup.esc="toggleShowNewTaskInput(bucket.id)"
class="input"
placeholder="Enter the new task text..."
type="text"
v-focus.always
v-model="newTaskText"
/>
</div>
<p class="help is-danger" v-if="newTaskError[bucket.id] && newTaskText === ''">
Please specify a title.
</p>
</div>
<a
@click="toggleShowNewTaskInput(bucket.id)"
class="button noshadow is-transparent is-fullwidth has-text-centered"
v-if="!showNewTaskInput[bucket.id]">
<span class="icon is-small">
<icon icon="plus"/>
</span>
<span v-if="bucket.tasks.length === 0">
<span v-if="bucket.tasks.length === 0">
Add a task
</span>
<span v-else>
<span v-else>
Add another task
</span>
</a>
</a>
</div>
</div>
</div>
<div class="bucket new-bucket" v-if="!loading && canWrite">
<input
:class="{'is-loading': loading}"
:disabled="loading"
@focusout="() => showNewBucketInput = false"
@keyup.enter="createNewBucket"
@keyup.esc="() => showNewBucketInput = false"
class="input"
placeholder="Enter the new bucket title..."
type="text"
v-focus.always
v-if="showNewBucketInput"
v-model="newBucketTitle"
/>
<a
@click="() => showNewBucketInput = true"
class="button noshadow is-transparent is-fullwidth has-text-centered" v-if="!showNewBucketInput">
<div class="bucket new-bucket" v-if="!loading && canWrite">
<input
:class="{'is-loading': loading}"
:disabled="loading"
@focusout="() => showNewBucketInput = false"
@keyup.enter="createNewBucket"
@keyup.esc="() => showNewBucketInput = false"
class="input"
placeholder="Enter the new bucket title..."
type="text"
v-focus.always
v-if="showNewBucketInput"
v-model="newBucketTitle"
/>
<a
@click="() => showNewBucketInput = true"
class="button noshadow is-transparent is-fullwidth has-text-centered" v-if="!showNewBucketInput">
<span class="icon is-small">
<icon icon="plus"/>
</span>
<span>
<span>
Create a new bucket
</span>
</a>
</a>
</div>
</div>
<!-- This router view is used to show the task popup while keeping the kanban board itself -->

View File

@ -1,23 +1,7 @@
<template>
<div :class="{ 'is-loading': taskService.loading}" class="loader-container task-view-container">
<div class="task-view">
<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="saveTaskOnChange()"
@keyup.ctrl.enter="saveTaskOnChange()"
class="title input"
contenteditable="true"
ref="taskTitle">
{{ task.title }}
</h1>
</div>
<heading v-model="task"/>
<h6 class="subtitle" v-if="parent && parent.namespace && parent.list">
{{ parent.namespace.title }} >
<router-link :to="{ name: listViewName, params: { listId: parent.list.id } }">
@ -67,7 +51,7 @@
:class="{ 'disabled': taskService.loading}"
:config="flatPickerConfig"
:disabled="taskService.loading || !canWrite"
@on-close="saveTask"
@on-close="() => saveTask()"
class="input"
placeholder="Click here to set a due date"
ref="dueDate"
@ -104,7 +88,7 @@
:class="{ 'disabled': taskService.loading}"
:config="flatPickerConfig"
:disabled="taskService.loading || !canWrite"
@on-close="saveTask"
@on-close="() => saveTask()"
class="input"
placeholder="Click here to set a start date"
ref="startDate"
@ -129,7 +113,7 @@
:class="{ 'disabled': taskService.loading}"
:config="flatPickerConfig"
:disabled="taskService.loading || !canWrite"
@on-close="saveTask"
@on-close="() => saveTask()"
class="input"
placeholder="Click here to set an end date"
ref="endDate"
@ -194,19 +178,11 @@
<!-- Description -->
<div :class="{ 'has-top-border': activeFields.labels }" class="details content description">
<h3>
<span class="icon is-grey">
<icon icon="align-left"/>
</span>
Description
</h3>
<editor
:is-edit-enabled="canWrite"
:upload-callback="attachmentUpload"
:upload-enabled="true"
@change="saveTask"
placeholder="Click here to enter a description..."
v-model="task.description"/>
<description
v-model="task"
:can-write="canWrite"
:attachment-upload="attachmentUpload"
/>
</div>
<!-- Attachments -->
@ -346,7 +322,8 @@
<!-- Created / Updated [by] -->
<p class="created">
Created <span v-tooltip="formatDate(task.created)">{{ formatDateSince(task.created) }}</span> by {{ task.createdBy.getDisplayName() }}
Created <span v-tooltip="formatDate(task.created)">{{ formatDateSince(task.created) }}</span>
by {{ task.createdBy.getDisplayName() }}
<template v-if="+new Date(task.created) !== +new Date(task.updated)">
<br/>
<!-- Computed properties to show the actual date every time it gets updated -->
@ -393,10 +370,10 @@ import Reminders from '../../components/tasks/partials/reminders'
import Comments from '../../components/tasks/partials/comments'
import router from '../../router'
import ListSearch from '../../components/tasks/partials/listSearch'
import description from '@/components/tasks/partials/description'
import ColorPicker from '../../components/input/colorPicker'
import attachmentUpload from '../../components/tasks/mixins/attachmentUpload'
import LoadingComponent from '../../components/misc/loading'
import ErrorComponent from '../../components/misc/error'
import heading from '@/components/tasks/partials/heading'
export default {
name: 'TaskDetailView',
@ -413,12 +390,8 @@ export default {
PrioritySelect,
Comments,
flatPickr,
editor: () => ({
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
}),
description,
heading,
},
mixins: [
attachmentUpload,
@ -441,10 +414,12 @@ export default {
taskColor: '',
showDeleteModal: false,
taskTitle: '',
descriptionChanged: false,
listViewName: 'list.list',
descriptionSaving: false,
descriptionRecentlySaved: false,
priorities: priorites,
flatPickerConfig: {
altFormat: 'j M Y H:i',
@ -519,7 +494,6 @@ export default {
.then(r => {
this.$set(this, 'task', r)
this.$store.commit('attachments/set', r.attachments)
this.taskTitle = this.task.title
this.taskColor = this.task.hexColor
this.setActiveFields()
this.setTitle(this.task.title)
@ -547,23 +521,7 @@ export default {
this.activeFields.attachments = this.task.attachments.length > 0
this.activeFields.relatedTasks = Object.keys(this.task.relatedTasks).length > 0
},
saveTaskOnChange() {
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(undoCallback = null) {
saveTask(showNotification = true, undoCallback = null) {
if (!this.canWrite) {
return
@ -584,15 +542,20 @@ export default {
this.$store.dispatch('tasks/update', this.task)
.then(r => {
this.$set(this, 'task', r)
this.setActiveFields()
if (!showNotification) {
return
}
let actions = []
if (undoCallback !== null) {
actions = [{
title: 'Undo',
callback: undoCallback,
}]
this.success({message: 'The task was saved successfully.'}, this, actions)
}
this.setActiveFields()
this.success({message: 'The task was saved successfully.'}, this, actions)
})
.catch(e => {
this.error(e, this)
@ -610,7 +573,7 @@ export default {
deleteTask() {
this.$store.dispatch('tasks/delete', this.task)
.then(() => {
this.success({message: 'The task been deleted successfully.'}, this)
this.success({message: 'The task has been deleted successfully.'}, this)
router.back()
})
.catch(e => {
@ -619,7 +582,7 @@ export default {
},
toggleTaskDone() {
this.task.done = !this.task.done
this.saveTask(() => this.toggleTaskDone())
this.saveTask(true, () => this.toggleTaskDone())
},
setDescriptionChanged(e) {
if (e.key === 'Enter' || e.key === 'Control') {