vikunja-frontend/src/components/tasks/partials/singleTaskInProject.vue
kolaente befa6f27bb feat: rename list to project everywhere
fix: project table view

fix: e2e tests

fix: typo in readme

fix: list view route

fix: don't wait until background is loaded for list to show

fix: rename component imports

fix: lint

fix: parse task text

fix: use list card grid

fix: use correct class names

fix: i18n keys

fix: load project

fix: task overview

fix: list view spacing

fix: find project

fix: setLoading when updating a project

fix: loading saved filter

fix: project store loading

fix: color picker import

fix: cypress tests

feat: migrate old list settings

chore: add const for project settings

fix: wrong projecten rename from lists

chore: rename unused variable

fix: editor list

fix: shortcut list class name

fix: pagination list class name

fix: notifications list class name

fix: list view variable name

chore: clarify comment

fix: i18n keys

fix: router imports

fix: comment

chore: remove debugging leftover

fix: remove duplicate variables

fix: change comment

fix: list view variable name

fix: list view css class name

fix: list item property name

fix: name update tasks function correctly

fix: update comment

fix: project create route

fix: list view class names

fix: list view component name

fix: result list class name

fix: animation class list name

fix: change debug log

fix: revert a few navigation changes

fix: use @ for imports of all views

fix: rename link share list class

fix: remove unused css class

fix: dynamically import project components again
2023-03-14 14:04:23 +00:00

429 lines
9.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<router-link
:to="taskDetailRoute"
:class="{'is-loading': taskService.loading}"
class="task loader-container"
>
<fancycheckbox
:disabled="(isArchived || disabled) && !canMarkAsDone"
@change="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>
<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>
<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"
class="dueDate"
@click.prevent.stop="showDefer = !showDefer"
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'
import TaskModel, { getHexColor } from '@/models/task'
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'
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'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import {success} from '@/message'
import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
import {useBaseStore} from '@/stores/base'
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
})
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%;
.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>