2020-03-23 22:24:14 +00:00
|
|
|
<template>
|
2020-09-05 20:35:52 +00:00
|
|
|
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
|
2022-07-12 09:59:39 +00:00
|
|
|
<fancycheckbox :disabled="(isArchived || disabled) && !canMarkAsDone" @change="markAsDone" v-model="task.done"/>
|
2022-09-15 11:56:14 +00:00
|
|
|
<ColorBubble
|
2021-02-20 14:54:12 +00:00
|
|
|
v-if="showListColor && listColor !== ''"
|
2022-09-15 11:56:14 +00:00
|
|
|
:color="listColor"
|
|
|
|
class="mr-1"
|
|
|
|
/>
|
2021-01-15 20:04:48 +00:00
|
|
|
<router-link
|
2021-10-03 13:54:24 +00:00
|
|
|
:to="taskDetailRoute"
|
2021-01-15 20:04:48 +00:00
|
|
|
:class="{ 'done': task.done}"
|
|
|
|
class="tasktext">
|
|
|
|
<span>
|
2020-08-01 13:17:10 +00:00
|
|
|
<router-link
|
2020-09-05 20:35:52 +00:00
|
|
|
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
|
|
|
class="task-list"
|
2022-09-15 11:56:14 +00:00
|
|
|
:class="{'mr-2': task.hexColor !== ''}"
|
2020-09-05 20:35:52 +00:00
|
|
|
v-if="showList && $store.getters['lists/getListById'](task.listId) !== null"
|
2021-06-23 23:24:57 +00:00
|
|
|
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})">
|
2020-08-01 13:17:10 +00:00
|
|
|
{{ $store.getters['lists/getListById'](task.listId).title }}
|
|
|
|
</router-link>
|
2020-05-11 14:52:58 +00:00
|
|
|
|
2022-09-15 11:56:14 +00:00
|
|
|
<ColorBubble
|
|
|
|
v-if="task.hexColor !== ''"
|
|
|
|
:color="task.getHexColor()"
|
|
|
|
class="mr-1"
|
|
|
|
/>
|
2021-01-17 10:36:57 +00:00
|
|
|
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
|
2020-08-01 13:17:10 +00:00
|
|
|
<span class="parent-tasks" v-if="typeof task.relatedTasks.parenttask !== 'undefined'">
|
|
|
|
<template v-for="(pt, i) in task.relatedTasks.parenttask">
|
|
|
|
{{ pt.title }}<template v-if="(i + 1) < task.relatedTasks.parenttask.length">, </template>
|
|
|
|
</template>
|
|
|
|
>
|
|
|
|
</span>
|
|
|
|
{{ task.title }}
|
2021-01-15 20:04:48 +00:00
|
|
|
</span>
|
2020-05-11 14:52:58 +00:00
|
|
|
|
2022-09-15 11:56:14 +00:00
|
|
|
<labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0"/>
|
2020-04-01 20:13:57 +00:00
|
|
|
<user
|
2020-09-05 20:35:52 +00:00
|
|
|
:avatar-size="27"
|
|
|
|
:is-inline="true"
|
|
|
|
:key="task.id + 'assignee' + a.id + i"
|
|
|
|
:show-username="false"
|
|
|
|
:user="a"
|
|
|
|
v-for="(a, i) in task.assignees"
|
2020-04-01 20:13:57 +00:00
|
|
|
/>
|
2022-05-10 23:14:38 +00:00
|
|
|
<BaseButton
|
2020-09-05 20:35:52 +00:00
|
|
|
v-if="+new Date(task.dueDate) > 0"
|
2022-05-10 23:15:08 +00:00
|
|
|
class="dueDate"
|
2022-05-10 23:14:38 +00:00
|
|
|
@click.prevent.stop="showDefer = !showDefer"
|
2022-06-23 00:58:00 +00:00
|
|
|
v-tooltip="formatDateLong(task.dueDate)"
|
2020-08-01 13:17:10 +00:00
|
|
|
>
|
2022-05-10 23:14:38 +00:00
|
|
|
<time
|
|
|
|
:datetime="formatISO(task.dueDate)"
|
|
|
|
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
|
|
|
class="is-italic"
|
|
|
|
:aria-expanded="showDefer ? 'true' : 'false'"
|
|
|
|
>
|
|
|
|
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
|
|
|
|
</time>
|
|
|
|
</BaseButton>
|
2020-08-01 13:17:10 +00:00
|
|
|
<transition name="fade">
|
2021-01-17 10:36:57 +00:00
|
|
|
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
|
2020-08-01 13:17:10 +00:00
|
|
|
</transition>
|
2021-10-05 05:43:10 +00:00
|
|
|
<priority-label :priority="task.priority" :done="task.done"/>
|
2020-12-30 21:20:33 +00:00
|
|
|
<span>
|
|
|
|
<span class="list-task-icon" v-if="task.attachments.length > 0">
|
|
|
|
<icon icon="paperclip"/>
|
|
|
|
</span>
|
|
|
|
<span class="list-task-icon" v-if="task.description">
|
|
|
|
<icon icon="align-left"/>
|
|
|
|
</span>
|
2021-11-01 16:06:03 +00:00
|
|
|
<span class="list-task-icon" v-if="task.repeatAfter.amount > 0">
|
|
|
|
<icon icon="history"/>
|
|
|
|
</span>
|
2020-12-30 21:20:33 +00:00
|
|
|
</span>
|
2021-09-29 18:31:14 +00:00
|
|
|
<checklist-summary :task="task"/>
|
2021-01-15 20:04:48 +00:00
|
|
|
</router-link>
|
2020-12-31 15:16:07 +00:00
|
|
|
<progress
|
|
|
|
class="progress is-small"
|
|
|
|
v-if="task.percentDone > 0"
|
|
|
|
:value="task.percentDone * 100" max="100">
|
|
|
|
{{ task.percentDone * 100 }}%
|
|
|
|
</progress>
|
2020-09-05 20:16:17 +00:00
|
|
|
<router-link
|
|
|
|
:to="{ name: 'list.list', params: { listId: task.listId } }"
|
2020-09-05 20:35:52 +00:00
|
|
|
class="task-list"
|
2020-09-05 21:42:34 +00:00
|
|
|
v-if="!showList && currentList.id !== task.listId && $store.getters['lists/getListById'](task.listId) !== null"
|
2021-06-23 23:24:57 +00:00
|
|
|
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})">
|
2020-09-05 20:16:17 +00:00
|
|
|
{{ $store.getters['lists/getListById'](task.listId).title }}
|
|
|
|
</router-link>
|
2022-05-10 23:14:38 +00:00
|
|
|
<BaseButton
|
2020-09-05 20:16:17 +00:00
|
|
|
:class="{'is-favorite': task.isFavorite}"
|
2020-09-05 20:35:52 +00:00
|
|
|
@click="toggleFavorite"
|
|
|
|
class="favorite">
|
2020-09-05 20:16:17 +00:00
|
|
|
<icon icon="star" v-if="task.isFavorite"/>
|
|
|
|
<icon :icon="['far', 'star']" v-else/>
|
2022-05-10 23:14:38 +00:00
|
|
|
</BaseButton>
|
2020-09-05 20:16:17 +00:00
|
|
|
<slot></slot>
|
|
|
|
</div>
|
2020-03-23 22:24:14 +00:00
|
|
|
</template>
|
|
|
|
|
2022-02-15 12:07:34 +00:00
|
|
|
<script lang="ts">
|
2022-07-30 15:51:09 +00:00
|
|
|
import {defineComponent, type PropType} from 'vue'
|
2022-02-15 12:07:59 +00:00
|
|
|
|
2022-09-06 09:36:01 +00:00
|
|
|
import TaskModel from '@/models/task'
|
|
|
|
import type {ITask} from '@/modelTypes/ITask'
|
2022-06-23 01:08:35 +00:00
|
|
|
import PriorityLabel from './priorityLabel.vue'
|
2020-09-05 20:35:52 +00:00
|
|
|
import TaskService from '../../../services/task'
|
2022-07-30 15:51:09 +00:00
|
|
|
import Labels from '@/components/tasks/partials/labels.vue'
|
|
|
|
import User from '@/components/misc/user.vue'
|
2022-05-10 23:14:38 +00:00
|
|
|
import BaseButton from '@/components/base/BaseButton.vue'
|
2022-06-23 01:08:35 +00:00
|
|
|
import Fancycheckbox from '../../input/fancycheckbox.vue'
|
|
|
|
import DeferTask from './defer-task.vue'
|
2021-01-17 10:36:57 +00:00
|
|
|
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
2021-01-30 20:45:54 +00:00
|
|
|
import {playPop} from '@/helpers/playPop'
|
2022-06-23 01:08:35 +00:00
|
|
|
import ChecklistSummary from './checklist-summary.vue'
|
2022-06-23 00:58:00 +00:00
|
|
|
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
2022-09-15 11:56:14 +00:00
|
|
|
import ColorBubble from '@/components/misc/colorBubble.vue'
|
2020-03-23 22:24:14 +00:00
|
|
|
|
2022-02-15 12:07:59 +00:00
|
|
|
export default defineComponent({
|
2020-09-05 20:35:52 +00:00
|
|
|
name: 'singleTaskInList',
|
|
|
|
data() {
|
|
|
|
return {
|
2021-09-08 09:59:38 +00:00
|
|
|
taskService: new TaskService(),
|
|
|
|
task: new TaskModel(),
|
2020-09-05 20:35:52 +00:00
|
|
|
showDefer: false,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
components: {
|
2022-09-15 11:56:14 +00:00
|
|
|
ColorBubble,
|
2022-05-10 23:14:38 +00:00
|
|
|
BaseButton,
|
2021-09-29 18:31:14 +00:00
|
|
|
ChecklistSummary,
|
2020-09-05 20:35:52 +00:00
|
|
|
DeferTask,
|
|
|
|
Fancycheckbox,
|
|
|
|
User,
|
|
|
|
Labels,
|
|
|
|
PriorityLabel,
|
|
|
|
},
|
|
|
|
props: {
|
|
|
|
theTask: {
|
2022-07-20 22:42:36 +00:00
|
|
|
type: Object as PropType<ITask>,
|
2020-09-05 20:35:52 +00:00
|
|
|
required: true,
|
2020-03-23 22:24:14 +00:00
|
|
|
},
|
2020-09-05 20:35:52 +00:00
|
|
|
isArchived: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
2020-03-23 22:24:14 +00:00
|
|
|
},
|
2020-09-05 20:35:52 +00:00
|
|
|
showList: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
2020-03-23 22:24:14 +00:00
|
|
|
},
|
2020-09-05 20:35:52 +00:00
|
|
|
disabled: {
|
|
|
|
type: Boolean,
|
|
|
|
default: false,
|
2020-03-23 22:24:14 +00:00
|
|
|
},
|
2021-02-20 14:54:12 +00:00
|
|
|
showListColor: {
|
|
|
|
type: Boolean,
|
|
|
|
default: true,
|
2021-07-17 21:21:46 +00:00
|
|
|
},
|
2022-07-12 09:59:39 +00:00
|
|
|
canMarkAsDone: {
|
|
|
|
type: Boolean,
|
|
|
|
default: true,
|
|
|
|
},
|
2020-09-05 20:35:52 +00:00
|
|
|
},
|
2021-08-23 19:18:12 +00:00
|
|
|
emits: ['task-updated'],
|
2020-09-05 20:35:52 +00:00
|
|
|
watch: {
|
|
|
|
theTask(newVal) {
|
|
|
|
this.task = newVal
|
2020-03-23 22:24:14 +00:00
|
|
|
},
|
2020-09-05 20:35:52 +00:00
|
|
|
},
|
|
|
|
mounted() {
|
|
|
|
this.task = this.theTask
|
2021-01-17 10:36:57 +00:00
|
|
|
document.addEventListener('click', this.hideDeferDueDatePopup)
|
2020-09-05 20:35:52 +00:00
|
|
|
},
|
2021-08-19 19:36:09 +00:00
|
|
|
beforeUnmount() {
|
2021-01-17 10:36:57 +00:00
|
|
|
document.removeEventListener('click', this.hideDeferDueDatePopup)
|
|
|
|
},
|
2020-09-05 20:35:52 +00:00
|
|
|
computed: {
|
2020-12-10 12:06:49 +00:00
|
|
|
listColor() {
|
|
|
|
const list = this.$store.getters['lists/getListById'](this.task.listId)
|
|
|
|
return list !== null ? list.hexColor : ''
|
|
|
|
},
|
2020-09-05 20:35:52 +00:00
|
|
|
currentList() {
|
|
|
|
return typeof this.$store.state.currentList === 'undefined' ? {
|
|
|
|
id: 0,
|
|
|
|
title: '',
|
|
|
|
} : this.$store.state.currentList
|
2020-09-05 20:16:17 +00:00
|
|
|
},
|
2021-10-03 13:54:24 +00:00
|
|
|
taskDetailRoute() {
|
|
|
|
return {
|
|
|
|
name: 'task.detail',
|
2022-04-24 15:30:36 +00:00
|
|
|
params: {id: this.task.id},
|
2022-01-19 22:58:54 +00:00
|
|
|
// TODO: re-enable opening task detail in modal
|
|
|
|
// state: { backdropView: this.$router.currentRoute.value.fullPath },
|
2021-10-03 13:54:24 +00:00
|
|
|
}
|
|
|
|
},
|
2020-09-05 20:35:52 +00:00
|
|
|
},
|
|
|
|
methods: {
|
2022-06-23 00:58:00 +00:00
|
|
|
formatDateSince,
|
|
|
|
formatISO,
|
|
|
|
formatDateLong,
|
|
|
|
|
2022-06-23 01:20:07 +00:00
|
|
|
async markAsDone(checked: boolean) {
|
2021-10-11 17:37:20 +00:00
|
|
|
const updateFunc = async () => {
|
|
|
|
const task = await this.taskService.update(this.task)
|
|
|
|
if (this.task.done) {
|
|
|
|
playPop()
|
|
|
|
}
|
|
|
|
this.task = task
|
|
|
|
this.$emit('task-updated', task)
|
|
|
|
this.$message.success({
|
|
|
|
message: this.task.done ?
|
|
|
|
this.$t('task.doneSuccess') :
|
|
|
|
this.$t('task.undoneSuccess'),
|
|
|
|
}, [{
|
|
|
|
title: 'Undo',
|
2022-02-26 12:18:44 +00:00
|
|
|
callback: () => this.undoDone(checked),
|
2021-10-11 17:37:20 +00:00
|
|
|
}])
|
2020-09-05 20:35:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (checked) {
|
|
|
|
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
|
|
|
|
} else {
|
2021-10-11 17:37:20 +00:00
|
|
|
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
|
2020-09-05 20:35:52 +00:00
|
|
|
}
|
|
|
|
},
|
2022-04-24 15:30:36 +00:00
|
|
|
|
2022-06-23 01:20:07 +00:00
|
|
|
undoDone(checked: boolean) {
|
2022-02-26 12:18:44 +00:00
|
|
|
this.task.done = !this.task.done
|
|
|
|
this.markAsDone(!checked)
|
|
|
|
},
|
2021-10-11 17:37:20 +00:00
|
|
|
|
|
|
|
async toggleFavorite() {
|
2020-09-05 20:35:52 +00:00
|
|
|
this.task.isFavorite = !this.task.isFavorite
|
2021-10-11 17:37:20 +00:00
|
|
|
this.task = await this.taskService.update(this.task)
|
|
|
|
this.$emit('task-updated', this.task)
|
|
|
|
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist')
|
2020-03-23 22:24:14 +00:00
|
|
|
},
|
2021-01-17 10:36:57 +00:00
|
|
|
hideDeferDueDatePopup(e) {
|
2021-10-11 17:37:20 +00:00
|
|
|
if (!this.showDefer) {
|
|
|
|
return
|
2021-01-17 10:36:57 +00:00
|
|
|
}
|
2021-10-11 17:37:20 +00:00
|
|
|
closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => {
|
|
|
|
this.showDefer = false
|
|
|
|
})
|
2021-01-17 10:36:57 +00:00
|
|
|
},
|
2020-09-05 20:35:52 +00:00
|
|
|
},
|
2022-02-15 12:07:59 +00:00
|
|
|
})
|
2020-03-23 22:24:14 +00:00
|
|
|
</script>
|
2021-10-18 12:22:47 +00:00
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
.task {
|
|
|
|
display: flex;
|
|
|
|
flex-wrap: wrap;
|
|
|
|
padding: .4rem;
|
|
|
|
transition: background-color $transition;
|
|
|
|
align-items: center;
|
|
|
|
cursor: pointer;
|
|
|
|
border-radius: $radius;
|
|
|
|
border: 2px solid transparent;
|
|
|
|
|
|
|
|
&:hover {
|
2021-11-22 21:12:54 +00:00
|
|
|
background-color: var(--grey-100);
|
2021-10-18 12:22:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.tasktext,
|
|
|
|
&.tasktext {
|
|
|
|
white-space: nowrap;
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
overflow: hidden;
|
|
|
|
display: inline-block;
|
|
|
|
flex: 1 0 50%;
|
|
|
|
|
2022-05-10 23:15:08 +00:00
|
|
|
.dueDate {
|
|
|
|
display: inline-block;
|
|
|
|
margin-left: 5px;
|
|
|
|
}
|
|
|
|
|
2021-10-18 12:22:47 +00:00
|
|
|
.overdue {
|
2021-11-22 21:12:54 +00:00
|
|
|
color: var(--danger);
|
2021-10-18 12:22:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.task-list {
|
|
|
|
width: auto;
|
2021-11-22 21:12:54 +00:00
|
|
|
color: var(--grey-400);
|
2021-10-18 12:22:47 +00:00
|
|
|
font-size: .9rem;
|
|
|
|
white-space: nowrap;
|
|
|
|
}
|
|
|
|
|
|
|
|
.avatar {
|
|
|
|
border-radius: 50%;
|
|
|
|
vertical-align: bottom;
|
|
|
|
margin-left: 5px;
|
|
|
|
height: 27px;
|
|
|
|
width: 27px;
|
|
|
|
}
|
|
|
|
|
|
|
|
.list-task-icon {
|
|
|
|
margin-left: 6px;
|
|
|
|
|
|
|
|
&:not(:first-of-type) {
|
|
|
|
margin-left: 8px;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
a {
|
2021-11-22 21:12:54 +00:00
|
|
|
color: var(--text);
|
2021-10-18 12:22:47 +00:00
|
|
|
transition: color ease $transition-duration;
|
|
|
|
|
|
|
|
&:hover {
|
2021-11-22 21:12:54 +00:00
|
|
|
color: var(--grey-900);
|
2021-10-18 12:22:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.favorite {
|
|
|
|
opacity: 0;
|
|
|
|
text-align: center;
|
|
|
|
width: 27px;
|
|
|
|
transition: opacity $transition, color $transition;
|
|
|
|
|
|
|
|
&:hover {
|
2021-11-22 21:12:54 +00:00
|
|
|
color: var(--warning);
|
2021-10-18 12:22:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
&.is-favorite {
|
|
|
|
opacity: 1;
|
2021-11-22 21:12:54 +00:00
|
|
|
color: var(--warning);
|
2021-10-18 12:22:47 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&:hover .favorite {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
.handle {
|
|
|
|
opacity: 0;
|
|
|
|
transition: opacity $transition;
|
|
|
|
margin-right: .25rem;
|
|
|
|
cursor: grab;
|
|
|
|
}
|
|
|
|
|
|
|
|
&:hover .handle {
|
|
|
|
opacity: 1;
|
|
|
|
}
|
|
|
|
|
2021-10-20 12:33:36 +00:00
|
|
|
:deep(.fancycheckbox) {
|
2021-10-18 12:22:47 +00:00
|
|
|
height: 18px;
|
|
|
|
padding-top: 0;
|
|
|
|
padding-right: .5rem;
|
|
|
|
|
|
|
|
span {
|
|
|
|
display: none;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.tasktext.done {
|
|
|
|
text-decoration: line-through;
|
2021-11-22 21:12:54 +00:00
|
|
|
color: var(--grey-500);
|
2021-10-18 12:22:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
span.parent-tasks {
|
2021-11-22 21:12:54 +00:00
|
|
|
color: var(--grey-500);
|
2021-10-18 12:22:47 +00:00
|
|
|
width: auto;
|
|
|
|
}
|
|
|
|
|
|
|
|
.remove {
|
2021-11-22 21:12:54 +00:00
|
|
|
color: var(--danger);
|
2021-10-18 12:22:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
input[type="checkbox"] {
|
|
|
|
vertical-align: middle;
|
|
|
|
}
|
|
|
|
|
|
|
|
.settings {
|
|
|
|
float: right;
|
|
|
|
width: 24px;
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
|
|
|
|
&.loader-container.is-loading:after {
|
|
|
|
top: calc(50% - 1rem);
|
|
|
|
left: calc(50% - 1rem);
|
|
|
|
width: 2rem;
|
|
|
|
height: 2rem;
|
2021-11-22 21:12:54 +00:00
|
|
|
border-left-color: var(--grey-300);
|
|
|
|
border-bottom-color: var(--grey-300);
|
2021-10-18 12:22:47 +00:00
|
|
|
}
|
2022-04-24 15:30:36 +00:00
|
|
|
|
|
|
|
.progress {
|
|
|
|
margin-bottom: 0;
|
|
|
|
}
|
2021-10-18 12:22:47 +00:00
|
|
|
}
|
|
|
|
</style>
|