frontend/src/components/tasks/partials/singleTaskInProject.vue

429 lines
9.5 KiB
Vue
Raw Normal View History

<template>
<router-link
:to="taskDetailRoute"
:class="{'is-loading': taskService.loading}"
class="task loader-container"
>
<fancycheckbox
:disabled="(isArchived || disabled) && !canMarkAsDone"
2023-03-07 16:59:12 +00:00
@update:model-value="markAsDone"
v-model="task.done"
/>
<ColorBubble
v-if="showProjectColor && projectColor !== '' && currentProject.id !== task.projectId"
:color="projectColor"
class="mr-1"
/>
<div
:class="{ 'done': task.done, 'show-project': showProject && project !== null}"
class="tasktext"
>
<span>
<router-link
v-if="showProject && project !== null"
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
class="task-project"
:class="{'mr-2': task.hexColor !== ''}"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})">
{{ project.title }}
</router-link>
2020-05-11 14:52:58 +00:00
<ColorBubble
v-if="task.hexColor !== ''"
:color="getHexColor(task.hexColor)"
class="mr-1"
/>
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
<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">,&nbsp;</template>
</template>
&rsaquo;
</span>
{{ task.title }}
</span>
2020-05-11 14:52:58 +00:00
<labels
v-if="task.labels.length > 0"
class="labels ml-2 mr-1"
:labels="task.labels"
/>
<User
v-for="(a, i) in task.assignees"
:avatar-size="27"
:is-inline="true"
:key="task.id + 'assignee' + a.id + i"
:show-username="false"
:user="a"
/>
<!-- FIXME: use popup -->
<BaseButton
v-if="+new Date(task.dueDate) > 0"
2022-05-10 23:15:08 +00:00
class="dueDate"
@click.prevent.stop="showDefer = !showDefer"
2022-06-23 00:58:00 +00:00
v-tooltip="formatDateLong(task.dueDate)"
>
<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>
<CustomTransition name="fade">
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
</CustomTransition>
<priority-label :priority="task.priority" :done="task.done"/>
<span>
<span class="project-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
</span>
<span class="project-task-icon" v-if="task.description">
<icon icon="align-left"/>
</span>
<span class="project-task-icon" v-if="task.repeatAfter.amount > 0">
<icon icon="history"/>
</span>
</span>
<checklist-summary :task="task"/>
</div>
<progress
class="progress is-small"
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100"
>
{{ task.percentDone * 100 }}%
</progress>
<router-link
v-if="!showProject && currentProject.id !== task.projectId && project !== null"
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
class="task-project"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
>
{{ project.title }}
</router-link>
<BaseButton
:class="{'is-favorite': task.isFavorite}"
@click.prevent="toggleFavorite"
class="favorite"
>
<icon icon="star" v-if="task.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
</BaseButton>
<slot />
</router-link>
</template>
<script setup lang="ts">
import {ref, watch, shallowReactive, toRef, type PropType, onMounted, onBeforeUnmount, computed} from 'vue'
import {useI18n} from 'vue-i18n'
2022-02-15 12:07:59 +00:00
import TaskModel, { getHexColor } from '@/models/task'
2022-09-06 09:36:01 +00:00
import type {ITask} from '@/modelTypes/ITask'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import Labels from '@/components/tasks/partials//labels.vue'
import DeferTask from '@/components/tasks/partials//defer-task.vue'
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
2022-07-30 15:51:09 +00:00
import User from '@/components/misc/user.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import ColorBubble from '@/components/misc/colorBubble.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import TaskService from '@/services/task'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
2022-06-23 00:58:00 +00:00
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import {success} from '@/message'
import {useProjectStore} from '@/stores/projects'
2022-09-02 09:15:29 +00:00
import {useNamespaceStore} from '@/stores/namespaces'
2022-09-24 13:20:40 +00:00
import {useBaseStore} from '@/stores/base'
2022-09-23 10:55:53 +00:00
import {useTaskStore} from '@/stores/tasks'
const props = defineProps({
theTask: {
type: Object as PropType<ITask>,
required: true,
},
isArchived: {
type: Boolean,
default: false,
},
showProject: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
showProjectColor: {
type: Boolean,
default: true,
},
canMarkAsDone: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['task-updated'])
const {t} = useI18n({useScope: 'global'})
const taskService = shallowReactive(new TaskService())
const task = ref<ITask>(new TaskModel())
const showDefer = ref(false)
const theTask = toRef(props, 'theTask')
watch(
theTask,
newVal => {
task.value = newVal
},
)
onMounted(() => {
task.value = theTask.value
document.addEventListener('click', hideDeferDueDatePopup)
})
onBeforeUnmount(() => {
document.removeEventListener('click', hideDeferDueDatePopup)
})
const baseStore = useBaseStore()
const projectStore = useProjectStore()
const taskStore = useTaskStore()
const namespaceStore = useNamespaceStore()
const project = computed(() => projectStore.getProjectById(task.value.projectId))
const projectColor = computed(() => project.value !== null ? project.value.hexColor : '')
const currentProject = computed(() => {
return typeof baseStore.currentProject === 'undefined' ? {
id: 0,
title: '',
} : baseStore.currentProject
2022-02-15 12:07:59 +00:00
})
const taskDetailRoute = computed(() => ({
name: 'task.detail',
params: {id: task.value.id},
// TODO: re-enable opening task detail in modal
// state: { backdropView: router.currentRoute.value.fullPath },
}))
async function markAsDone(checked: boolean) {
const updateFunc = async () => {
const newTask = await taskStore.update(task.value)
task.value = newTask
emit('task-updated', newTask)
success({
message: task.value.done ?
t('task.doneSuccess') :
t('task.undoneSuccess'),
}, [{
title: t('task.undo'),
callback: () => undoDone(checked),
}])
}
if (checked) {
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
} else {
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
}
}
function undoDone(checked: boolean) {
task.value.done = !task.value.done
markAsDone(!checked)
}
async function toggleFavorite() {
task.value.isFavorite = !task.value.isFavorite
task.value = await taskService.update(task.value)
emit('task-updated', task.value)
namespaceStore.loadNamespacesIfFavoritesDontExist()
}
const deferDueDate = ref<typeof DeferTask | null>(null)
function hideDeferDueDatePopup(e) {
if (!showDefer.value) {
return
}
closeWhenClickedOutside(e, deferDueDate.value.$el, () => {
showDefer.value = false
})
}
</script>
<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;
color: var(--text);
transition: color ease $transition-duration;
&:hover {
color: var(--grey-900);
background-color: var(--grey-100);
}
.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;
}
.overdue {
color: var(--danger);
}
}
.task-project {
width: auto;
color: var(--grey-400);
font-size: .9rem;
white-space: nowrap;
}
.avatar {
border-radius: 50%;
vertical-align: bottom;
margin-left: 5px;
height: 27px;
width: 27px;
}
.project-task-icon {
margin-left: 6px;
&:not(:first-of-type) {
margin-left: 8px;
}
}
.favorite {
opacity: 1;
text-align: center;
width: 27px;
transition: opacity $transition, color $transition;
&:hover {
color: var(--warning);
}
&.is-favorite {
opacity: 1;
color: var(--warning);
}
}
.handle {
opacity: 1;
transition: opacity $transition;
margin-right: .25rem;
cursor: grab;
}
@media(hover: hover) and (pointer: fine) {
& .favorite,
& .handle {
opacity: 0;
}
&:hover .favorite,
&:hover .handle {
opacity: 1;
}
}
:deep(.fancycheckbox) {
height: 18px;
padding-top: 0;
padding-right: .5rem;
span {
display: none;
}
}
.tasktext.done {
text-decoration: line-through;
color: var(--grey-500);
}
span.parent-tasks {
color: var(--grey-500);
width: auto;
}
.show-project .parent-tasks {
padding-left: .25rem;
}
.remove {
color: var(--danger);
}
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;
border-left-color: var(--grey-300);
border-bottom-color: var(--grey-300);
}
.progress {
margin-bottom: 0;
}
}
</style>