vikunja-frontend/src/views/tasks/TaskDetailView.vue

618 lines
19 KiB
Vue
Raw Normal View History

2019-11-24 14:16:24 +01:00
<template>
<div :class="{ 'is-loading': taskService.loading}" class="loader-container task-view-container">
2019-11-24 14:16:24 +01:00
<div class="task-view">
<heading v-model="task" :can-write="canWrite" ref="heading"/>
<h6 class="subtitle" v-if="parent && parent.namespace && parent.list">
{{ parent.namespace.title }} >
<router-link :to="{ name: listViewName, params: { listId: parent.list.id } }">
{{ parent.list.title }}
</router-link>
</h6>
2019-11-24 14:16:24 +01:00
<!-- Content and buttons -->
<div class="columns">
<!-- Content -->
<div :class="{'is-two-thirds': canWrite}" class="column">
2019-11-24 14:16:24 +01:00
<div class="columns details">
<div class="column assignees" v-if="activeFields.assignees">
<!-- Assignees -->
<div class="detail-title">
<icon icon="users"/>
Assignees
</div>
<edit-assignees
:disabled="!canWrite"
:list-id="task.listId"
:task-id="task.id"
ref="assignees"
v-model="task.assignees"
2019-11-24 14:16:24 +01:00
/>
</div>
<transition name="flash-background" appear>
<div class="column" v-if="activeFields.priority">
<!-- Priority -->
<div class="detail-title">
<icon :icon="['far', 'star']"/>
Priority
</div>
<priority-select
:disabled="!canWrite"
@change="saveTask"
ref="priority"
v-model="task.priority"/>
2019-11-24 14:16:24 +01:00
</div>
</transition>
<transition name="flash-background" appear>
<div class="column" v-if="activeFields.dueDate">
<!-- Due Date -->
<div class="detail-title">
<icon icon="calendar"/>
Due Date
</div>
<div class="date-input">
<datepicker
v-model="task.dueDate"
@close-on-change="() => saveTask()"
choose-date-label="Click here to set a due date"
:disabled="taskService.loading || !canWrite"
ref="dueDate"
/>
<a @click="() => {task.dueDate = null;saveTask()}" v-if="task.dueDate && canWrite" class="remove">
<span class="icon is-small">
<icon icon="times"></icon>
</span>
</a>
</div>
2019-11-24 14:16:24 +01:00
</div>
</transition>
<transition name="flash-background" appear>
<div class="column" v-if="activeFields.percentDone">
<!-- Percent Done -->
<div class="detail-title">
<icon icon="percent"/>
Percent Done
</div>
<percent-done-select
:disabled="!canWrite"
@change="saveTask"
ref="percentDone"
v-model="task.percentDone"/>
2019-11-24 14:16:24 +01:00
</div>
</transition>
<transition name="flash-background" appear>
<div class="column" v-if="activeFields.startDate">
<!-- Start Date -->
<div class="detail-title">
<icon icon="calendar-week"/>
Start Date
</div>
<div class="date-input">
<datepicker
v-model="task.startDate"
@close-on-change="() => saveTask()"
choose-date-label="Click here to set a start date"
:disabled="taskService.loading || !canWrite"
ref="startDate"
/>
<a @click="() => {task.startDate = null;saveTask()}" v-if="task.startDate && canWrite" class="remove">
<span class="icon is-small">
<icon icon="times"></icon>
</span>
</a>
</div>
2019-11-24 14:16:24 +01:00
</div>
</transition>
<transition name="flash-background" appear>
<div class="column" v-if="activeFields.endDate">
<!-- End Date -->
<div class="detail-title">
<icon icon="calendar-week"/>
End Date
</div>
<div class="date-input">
<datepicker
v-model="task.endDate"
@close-on-change="() => saveTask()"
choose-date-label="Click here to set an end date"
:disabled="taskService.loading || !canWrite"
ref="endDate"
/>
<a @click="() => {task.endDate = null;saveTask()}" v-if="task.endDate && canWrite" class="remove">
<span class="icon is-small">
<icon icon="times"></icon>
</span>
</a>
</div>
2019-11-24 14:16:24 +01:00
</div>
</transition>
<transition name="flash-background" appear>
<div class="column" v-if="activeFields.reminders">
<!-- Reminders -->
<div class="detail-title">
<icon icon="history"/>
Reminders
</div>
<reminders
:disabled="!canWrite"
@change="saveTask"
ref="reminders"
v-model="task.reminderDates"/>
2019-11-24 14:16:24 +01:00
</div>
</transition>
<transition name="flash-background" appear>
<div class="column" v-if="activeFields.repeatAfter">
<!-- Repeat after -->
<div class="detail-title">
<icon :icon="['far', 'clock']"/>
Repeat
</div>
<repeat-after
:disabled="!canWrite"
@change="saveTask"
ref="repeatAfter"
v-model="task"/>
2019-11-24 14:16:24 +01:00
</div>
</transition>
<transition name="flash-background" appear>
<div class="column" v-if="activeFields.color">
<!-- Color -->
<div class="detail-title">
<icon icon="fill-drip"/>
Color
</div>
<color-picker
@change="saveTask"
menu-position="bottom"
ref="color"
v-model="taskColor"/>
</div>
</transition>
2019-11-24 14:16:24 +01:00
</div>
<!-- Labels -->
<div class="labels-list details" v-if="activeFields.labels">
<div class="detail-title">
<span class="icon is-grey">
<icon icon="tags"/>
</span>
Labels
</div>
<edit-labels :disabled="!canWrite" :task-id="taskId" ref="labels" v-model="task.labels"/>
2019-11-24 14:16:24 +01:00
</div>
<!-- Description -->
2021-01-16 21:55:43 +01:00
<div class="details content description">
<description
v-model="task"
:can-write="canWrite"
:attachment-upload="attachmentUpload"
/>
2019-11-24 14:16:24 +01:00
</div>
<!-- Attachments -->
2021-01-16 21:55:43 +01:00
<div class="content attachments" v-if="activeFields.attachments">
2019-11-24 14:16:24 +01:00
<attachments
:edit-enabled="canWrite"
:task-id="taskId"
ref="attachments"
2019-11-24 14:16:24 +01:00
/>
</div>
<!-- Related Tasks -->
2021-01-16 21:55:43 +01:00
<div class="content details mb-0" v-if="activeFields.relatedTasks">
2019-11-24 14:16:24 +01:00
<h3>
<span class="icon is-grey">
<icon icon="tasks"/>
</span>
Related Tasks
</h3>
<related-tasks
:edit-enabled="canWrite"
:initial-related-tasks="task.relatedTasks"
:list-id="task.listId"
:show-no-relations-notice="true"
:task-id="taskId"
ref="relatedTasks"
2019-11-24 14:16:24 +01:00
/>
</div>
2020-04-18 14:39:56 +02:00
<!-- Move Task -->
2021-01-16 21:55:43 +01:00
<div class="content details" v-if="activeFields.moveList">
2020-04-18 14:39:56 +02:00
<h3>
<span class="icon is-grey">
<icon icon="list"/>
</span>
Move task to a different list
2020-04-18 14:39:56 +02:00
</h3>
<div class="field has-addons">
<div class="control is-expanded">
<list-search @selected="changeList" ref="moveList"/>
2020-04-18 14:39:56 +02:00
</div>
</div>
</div>
<!-- Comments -->
<comments :can-write="canWrite" :task-id="taskId"/>
2019-11-24 14:16:24 +01:00
</div>
<div class="column is-one-third action-buttons" v-if="canWrite">
<a
2021-01-17 13:04:49 +01:00
:class="{'is-success': !task.done, 'has-no-shadow': !task.done}"
@click="toggleTaskDone()"
2021-01-17 13:04:49 +01:00
class="button is-outlined has-no-border">
<span class="icon is-small"><icon icon="check-double"/></span>
<template v-if="task.done">
Mark as undone
</template>
<template v-else>
Done!
</template>
</a>
<a
@click="setFieldActive('assignees')"
@shortkey="setFieldActive('assignees')"
class="button"
2020-11-11 21:31:11 +01:00
v-shortkey="['a']">
2019-11-24 14:16:24 +01:00
<span class="icon is-small"><icon icon="users"/></span>
Assign this task to a user
</a>
<a
@click="setFieldActive('labels')"
@shortkey="setFieldActive('labels')"
class="button"
2020-11-11 21:31:11 +01:00
v-shortkey="['l']">
2019-11-24 14:16:24 +01:00
<span class="icon is-small"><icon icon="tags"/></span>
Add labels
</a>
2021-01-16 20:51:20 +01:00
<a @click="setFieldActive('priority')" class="button">
<span class="icon is-small"><icon :icon="['far', 'star']"/></span>
Set Priority
</a>
<a
@click="setFieldActive('dueDate')"
@shortkey="setFieldActive('dueDate')"
class="button"
2020-11-11 21:31:11 +01:00
v-shortkey="['d']">
2019-11-24 14:16:24 +01:00
<span class="icon is-small"><icon icon="calendar"/></span>
Set Due Date
</a>
<a @click="setFieldActive('startDate')" class="button">
2019-11-24 14:16:24 +01:00
<span class="icon is-small"><icon icon="calendar-week"/></span>
Set a Start Date
</a>
<a @click="setFieldActive('endDate')" class="button">
2019-11-24 14:16:24 +01:00
<span class="icon is-small"><icon icon="calendar-week"/></span>
Set an End Date
</a>
2021-01-16 20:51:20 +01:00
<a @click="setFieldActive('reminders')" class="button">
<span class="icon is-small"><icon icon="history"/></span>
Set Reminders
</a>
<a @click="setFieldActive('repeatAfter')" class="button">
2019-11-24 14:16:24 +01:00
<span class="icon is-small"><icon :icon="['far', 'clock']"/></span>
Set a repeating interval
</a>
<a @click="setFieldActive('percentDone')" class="button">
<span class="icon is-small"><icon icon="percent"/></span>
Set Percent Done
</a>
<a
@click="setFieldActive('attachments')"
@shortkey="setFieldActive('attachments')"
class="button"
2020-11-11 21:31:11 +01:00
v-shortkey="['f']">
<span class="icon is-small"><icon icon="paperclip"/></span>
Add attachments
</a>
<a
@click="setFieldActive('relatedTasks')"
@shortkey="setFieldActive('relatedTasks')"
class="button"
2020-11-11 21:31:11 +01:00
v-shortkey="['r']">
<span class="icon is-small"><icon icon="tasks"/></span>
Add task relations
</a>
<a @click="setFieldActive('moveList')" class="button">
2020-04-18 14:39:56 +02:00
<span class="icon is-small"><icon icon="list"/></span>
Move task
2020-04-18 14:39:56 +02:00
</a>
<a @click="setFieldActive('color')" class="button">
<span class="icon is-small"><icon icon="fill-drip"/></span>
Set task color
</a>
2021-01-17 13:21:58 +01:00
<a @click="showDeleteModal = true" class="button is-danger is-outlined has-no-shadow has-no-border">
<span class="icon is-small"><icon icon="trash-alt"/></span>
Delete task
</a>
2020-10-25 12:25:08 +01:00
<!-- Created / Updated [by] -->
<p class="created">
Created <span v-tooltip="formatDate(task.created)">{{ formatDateSince(task.created) }}</span>
by {{ task.createdBy.getDisplayName() }}
2020-10-25 12:25:08 +01:00
<template v-if="+new Date(task.created) !== +new Date(task.updated)">
<br/>
<!-- Computed properties to show the actual date every time it gets updated -->
Updated <span v-tooltip="updatedFormatted">{{ updatedSince }}</span>
</template>
2020-11-28 15:52:15 +01:00
<template v-if="task.done">
<br/>
Done <span v-tooltip="doneFormatted">{{ doneSince }}</span>
</template>
2020-10-25 12:25:08 +01:00
</p>
2019-11-24 14:16:24 +01:00
</div>
</div>
</div>
<modal
@close="showDeleteModal = false"
@submit="deleteTask()"
v-if="showDeleteModal">
<span slot="header">Delete this task</span>
<p slot="text">
Are you sure you want to remove this task? <br/>
This will also remove all attachments, reminders and relations associated with this task and
<b>cannot be undone!</b>
</p>
</modal>
2019-11-24 14:16:24 +01:00
</div>
</template>
<script>
import TaskService from '../../services/task'
import TaskModel from '../../models/task'
import relationKinds from '../../models/relationKinds.json'
2019-11-24 14:16:24 +01:00
import priorites from '../../models/priorities.json'
import rights from '../../models/rights.json'
2019-11-24 14:16:24 +01:00
import PrioritySelect from '../../components/tasks/partials/prioritySelect'
import PercentDoneSelect from '../../components/tasks/partials/percentDoneSelect'
import EditLabels from '../../components/tasks/partials/editLabels'
import EditAssignees from '../../components/tasks/partials/editAssignees'
import Attachments from '../../components/tasks/partials/attachments'
import RelatedTasks from '../../components/tasks/partials/relatedTasks'
import RepeatAfter from '../../components/tasks/partials/repeatAfter'
import Reminders from '../../components/tasks/partials/reminders'
import Comments from '../../components/tasks/partials/comments'
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 heading from '@/components/tasks/partials/heading'
Better reminders (#308) Fix setting the new reminder component to null after adding a new date Add "close on change" event which only fires if the component closed and the value actually changed Hide the "today" option after 21:00 Add "confirm" button to close the component Use disabled in reminders Add a disabled property to the datepicker Cleanup workarounds for flatpickr Use the new datepicker for end dates Use the new datepicker for start date Use the new datepicker for due dates Mobile styling Format Sync flatpickr when clicking on choose a date Make sure to only hide the popup when not clicked something inside of it Make flatpickr dates work Use datepicker component for reminders Merge branch 'master' into feature/better-reminders Fix bottom padding of inline flatpickr Set time Add method to calculate the neares time Move time helpers in separate folder Remove separate flatpickr date Cleanup Set the flatpickr date when setting changing the date Better formatting of the chosen date Bubble Set date when choosing one Fix test Show correct weekday in preview Change hover background color Make label to show if selected date is null configurable Use a different icon for weekend Ignore test files when linting Add tests to dron Move day interval calculation to separate file and test it Add next date calculation Add basic date picker component Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/308 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
2020-11-28 14:59:27 +01:00
import Datepicker from '@/components/input/datepicker'
2019-11-24 14:16:24 +01:00
export default {
name: 'TaskDetailView',
components: {
Better reminders (#308) Fix setting the new reminder component to null after adding a new date Add "close on change" event which only fires if the component closed and the value actually changed Hide the "today" option after 21:00 Add "confirm" button to close the component Use disabled in reminders Add a disabled property to the datepicker Cleanup workarounds for flatpickr Use the new datepicker for end dates Use the new datepicker for start date Use the new datepicker for due dates Mobile styling Format Sync flatpickr when clicking on choose a date Make sure to only hide the popup when not clicked something inside of it Make flatpickr dates work Use datepicker component for reminders Merge branch 'master' into feature/better-reminders Fix bottom padding of inline flatpickr Set time Add method to calculate the neares time Move time helpers in separate folder Remove separate flatpickr date Cleanup Set the flatpickr date when setting changing the date Better formatting of the chosen date Bubble Set date when choosing one Fix test Show correct weekday in preview Change hover background color Make label to show if selected date is null configurable Use a different icon for weekend Ignore test files when linting Add tests to dron Move day interval calculation to separate file and test it Add next date calculation Add basic date picker component Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/308 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
2020-11-28 14:59:27 +01:00
Datepicker,
ColorPicker,
ListSearch,
Reminders,
RepeatAfter,
RelatedTasks,
Attachments,
EditAssignees,
EditLabels,
PercentDoneSelect,
PrioritySelect,
Comments,
description,
heading,
},
mixins: [
attachmentUpload,
],
data() {
return {
taskId: Number(this.$route.params.id),
taskService: TaskService,
task: TaskModel,
relationKinds: relationKinds,
// We doubled the task color property here because verte does not have a real change property, leading
// to the color property change being triggered when the # is removed from it, leading to an update,
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
// updated, changed, updated and so on.
// To prevent this, we put the task color property in a seperate value which is set to the task color
// when it is saved and loaded.
taskColor: '',
2019-11-24 14:16:24 +01:00
showDeleteModal: false,
descriptionChanged: false,
listViewName: 'list.list',
2019-11-24 14:16:24 +01:00
priorities: priorites,
activeFields: {
assignees: false,
priority: false,
dueDate: false,
percentDone: false,
startDate: false,
endDate: false,
reminders: false,
repeatAfter: false,
labels: false,
attachments: false,
relatedTasks: false,
moveList: false,
color: false,
},
}
},
watch: {
'$route': 'loadTask',
},
created() {
this.taskService = new TaskService()
this.task = new TaskModel()
},
mounted() {
// Build the list path from the task detail name to send the user to the view they came from.
const parts = this.$route.name.split('.')
if (parts.length > 2 && parts[2] === 'detail') {
this.listViewName = `list.${parts[1]}`
}
this.loadTask()
},
computed: {
parent() {
if (!this.task.listId) {
return {
namespace: null,
list: null,
}
}
if (!this.$store.getters['namespaces/getListAndNamespaceById']) {
return null
}
return this.$store.getters['namespaces/getListAndNamespaceById'](this.task.listId)
},
canWrite() {
return this.task && this.task.maxRight && this.task.maxRight > rights.READ
},
2020-10-25 12:25:08 +01:00
updatedSince() {
return this.formatDateSince(this.task.updated)
},
updatedFormatted() {
return this.formatDate(this.task.updated)
},
2020-11-28 15:52:15 +01:00
doneSince() {
return this.formatDateSince(this.task.doneAt)
},
doneFormatted() {
return this.formatDate(this.task.doneAt)
},
},
methods: {
loadTask() {
this.taskId = Number(this.$route.params.id)
this.taskService.get({id: this.taskId})
.then(r => {
this.$set(this, 'task', r)
this.$store.commit('attachments/set', r.attachments)
this.taskColor = this.task.hexColor
this.setActiveFields()
this.setTitle(this.task.title)
})
.catch(e => {
this.error(e, this)
})
.finally(() => {
this.scrollToHeading()
})
},
scrollToHeading() {
this.$refs.heading.$el.scrollIntoView({block: 'center'})
},
setActiveFields() {
2020-02-08 18:37:23 +01:00
this.task.startDate = this.task.startDate ? this.task.startDate : null
this.task.endDate = this.task.endDate ? this.task.endDate : null
2020-02-08 18:37:23 +01:00
// Set all active fields based on values in the model
this.activeFields.assignees = this.task.assignees.length > 0
this.activeFields.priority = this.task.priority !== priorites.UNSET
this.activeFields.dueDate = this.task.dueDate !== null
this.activeFields.percentDone = this.task.percentDone > 0
this.activeFields.startDate = this.task.startDate !== null
this.activeFields.endDate = this.task.endDate !== null
this.activeFields.reminders = this.task.reminderDates.length > 0
this.activeFields.repeatAfter = this.task.repeatAfter.amount > 0
this.activeFields.labels = this.task.labels.length > 0
this.activeFields.attachments = this.task.attachments.length > 0
this.activeFields.relatedTasks = Object.keys(this.task.relatedTasks).length > 0
},
saveTask(showNotification = true, undoCallback = null) {
if (!this.canWrite) {
return
}
// We're doing the whole update in a nextTick because sometimes race conditions can occur when
// setting the due date on mobile which leads to no due date change being saved.
this.$nextTick(() => {
this.task.hexColor = this.taskColor
// If no end date is being set, but a start date and due date,
// use the due date as the end date
if (this.task.endDate === null && this.task.startDate !== null && this.task.dueDate !== null) {
this.task.endDate = this.task.dueDate
}
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)
})
.catch(e => {
this.error(e, this)
})
})
},
setFieldActive(fieldName) {
this.activeFields[fieldName] = true
this.$nextTick(() => {
if (this.$refs[fieldName]) {
this.$refs[fieldName].$el.focus();
// scroll the field to the center of the screen if not in viewport already
const boundingRect = this.$refs[fieldName].$el.getBoundingClientRect();
if (boundingRect.top > (window.scrollY + window.innerHeight) || boundingRect.top < window.scrollY)
this.$refs[fieldName].$el.scrollIntoView({behavior: "smooth", block: "center", inline: "nearest"});
}
})
},
deleteTask() {
this.$store.dispatch('tasks/delete', this.task)
.then(() => {
this.success({message: 'The task has been deleted successfully.'}, this)
Frontend Testing With Cypress (#313) Wait until the request is finished Wait for the newly created task exists in the dom Wait until the login request is done Wait until the list request is done Make sure no user token is in local storage when trying to register Make sure to always upload test results Disable capturing videos of test runs in CI Add uploading test result screenshots from ci Assert a success notification is shown after creating a new list Change input element locators Fix testing for favorite lists Make sure faked usernames are always random Make sure the tests work Make sure to use node 12 everywhere in ci Add docs Fix setting api url for running tests Use a working node version Ignore cypress screenshots and videos Set cache folders Explicitly ignore cypress files when running unit tests Trigger Drone Only run unit tests with yarn test:unit Add serve dist command to serve built static files Trigger Drone Fix cypress image Change cypress image Unify test & build step back again to prevent double installation of dependencies Add cache location config Move test steps to separate pipeline Run cypress tests in drone Fix all tests Make all factory methods static Use factories everywhere Cleanup Add tests for the editor Add tests for viewing link shares Fix seed Add test to make sure settings elements are hidden if the user does not have the right to edit the current list Co-authored-by: kolaente <k@knt.li> Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/313 Co-Authored-By: konrad <konrad@kola-entertainments.de> Co-Committed-By: konrad <konrad@kola-entertainments.de>
2020-12-10 14:52:35 +01:00
this.$router.push({name: 'list.index', params: {listId: this.task.listId}})
})
.catch(e => {
this.error(e, this)
})
},
toggleTaskDone() {
this.task.done = !this.task.done
this.saveTask(true, () => this.toggleTaskDone())
},
setDescriptionChanged(e) {
if (e.key === 'Enter' || e.key === 'Control') {
return
}
this.descriptionChanged = true
},
saveTaskIfDescriptionChanged() {
// We want to only save the description if it was changed.
// Since we can either trigger this with ctrl+enter or @change, it would be possible to save a task first
// with ctrl+enter and then with @change although nothing changed since the last save when @change gets fired.
// To only save one time we added this method.
if (this.descriptionChanged) {
this.descriptionChanged = false
2020-04-18 14:39:56 +02:00
this.saveTask()
}
},
changeList(list) {
this.task.listId = list.id
this.saveTask()
2019-11-24 14:16:24 +01:00
},
},
}
2019-11-24 14:16:24 +01:00
</script>