feat(list view): show subtasks nested
continuous-integration/drone/push Build is failing Details

Resolves #363
This commit is contained in:
kolaente 2023-09-07 13:43:03 +02:00
parent 842e2c2811
commit e41712647d
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
2 changed files with 118 additions and 90 deletions

View File

@ -1,25 +1,26 @@
<template> <template>
<div <div>
:class="{'is-loading': taskService.loading}"
class="task loader-container"
@click.stop.self="openTaskDetail"
>
<fancycheckbox
:disabled="(isArchived || disabled) && !canMarkAsDone"
@update:model-value="markAsDone"
v-model="task.done"
/>
<ColorBubble
v-if="showProjectColor && projectColor !== '' && currentProject?.id !== task.projectId"
:color="projectColor"
class="mr-1"
/>
<div <div
:class="{ 'done': task.done, 'show-project': showProject && project}" :class="{'is-loading': taskService.loading}"
class="tasktext" class="task loader-container"
@click.stop.self="openTaskDetail"
> >
<fancycheckbox
:disabled="(isArchived || disabled) && !canMarkAsDone"
@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}"
class="tasktext"
>
<span> <span>
<router-link <router-link
v-if="showProject && typeof project !== 'undefined'" v-if="showProject && typeof project !== 'undefined'"
@ -38,14 +39,6 @@
/> />
<priority-label :priority="task.priority" :done="task.done"/> <priority-label :priority="task.priority" :done="task.done"/>
<!-- 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>
<router-link <router-link
:to="taskDetailRoute" :to="taskDetailRoute"
@ -57,41 +50,41 @@
</router-link> </router-link>
</span> </span>
<labels <labels
v-if="task.labels.length > 0" v-if="task.labels.length > 0"
class="labels ml-2 mr-1" class="labels ml-2 mr-1"
:labels="task.labels" :labels="task.labels"
/> />
<assignee-list <assignee-list
v-if="task.assignees.length > 0" v-if="task.assignees.length > 0"
:assignees="task.assignees" :assignees="task.assignees"
:avatar-size="25" :avatar-size="25"
class="ml-1" class="ml-1"
:inline="true" :inline="true"
/> />
<!-- FIXME: use popup --> <!-- FIXME: use popup -->
<BaseButton <BaseButton
v-if="+new Date(task.dueDate) > 0" v-if="+new Date(task.dueDate) > 0"
class="dueDate" class="dueDate"
@click.prevent.stop="showDefer = !showDefer" @click.prevent.stop="showDefer = !showDefer"
v-tooltip="formatDateLong(task.dueDate)" 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: dueDateFormatted}) }} <time
</time> :datetime="formatISO(task.dueDate)"
</BaseButton> :class="{'overdue': task.dueDate <= new Date() && !task.done}"
<CustomTransition name="fade"> class="is-italic"
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/> :aria-expanded="showDefer ? 'true' : 'false'"
</CustomTransition> >
{{ $t('task.detail.due', {at: dueDateFormatted}) }}
</time>
</BaseButton>
<CustomTransition name="fade">
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
</CustomTransition>
<span> <span>
<span class="project-task-icon" v-if="task.attachments.length > 0"> <span class="project-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/> <icon icon="paperclip"/>
</span> </span>
@ -103,35 +96,51 @@
</span> </span>
</span> </span>
<checklist-summary :task="task"/> <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"
: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="toggleFavorite"
class="favorite"
>
<icon icon="star" v-if="task.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
</BaseButton>
<slot/>
</div> </div>
<template v-if="typeof task.relatedTasks?.subtask !== 'undefined'">
<progress <template v-for="subtask in task.relatedTasks.subtask">
class="progress is-small" <template v-if="getTaskById(subtask.id)">
v-if="task.percentDone > 0" <single-task-in-project
:value="task.percentDone * 100" max="100" :key="subtask.id"
> :the-task="getTaskById(subtask.id)"
{{ task.percentDone * 100 }}% :show-project-color="showProjectColor"
</progress> :disabled="disabled"
:can-mark-as-done="canMarkAsDone"
<router-link :all-tasks="allTasks"
v-if="!showProject && currentProject?.id !== task.projectId && project" class="ml-5"
:to="{ name: 'project.list', params: { projectId: task.projectId } }" />
class="task-project" </template>
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})" </template>
> </template>
{{ project.title }}
</router-link>
<BaseButton
:class="{'is-favorite': task.isFavorite}"
@click="toggleFavorite"
class="favorite"
>
<icon icon="star" v-if="task.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
</BaseButton>
<slot/>
</div> </div>
</template> </template>
@ -173,6 +182,7 @@ const {
disabled = false, disabled = false,
showProjectColor = false, showProjectColor = false,
canMarkAsDone = true, canMarkAsDone = true,
allTasks = [],
} = defineProps<{ } = defineProps<{
theTask: ITask, theTask: ITask,
isArchived?: boolean, isArchived?: boolean,
@ -180,8 +190,17 @@ const {
disabled?: boolean, disabled?: boolean,
showProjectColor?: boolean, showProjectColor?: boolean,
canMarkAsDone?: boolean, canMarkAsDone?: boolean,
allTasks?: ITask[],
}>() }>()
function getTaskById(taskId: number): ITask | undefined {
if (typeof allTasks === 'undefined' || allTasks.length === 0) {
return null
}
return allTasks.find(t => t.id === taskId)
}
const emit = defineEmits(['task-updated']) const emit = defineEmits(['task-updated'])
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -289,6 +308,7 @@ function hideDeferDueDatePopup(e) {
} }
const taskLink = ref<HTMLElement | null>(null) const taskLink = ref<HTMLElement | null>(null)
function openTaskDetail() { function openTaskDetail() {
const isTextSelected = window.getSelection().toString() const isTextSelected = window.getSelection().toString()
if (!isTextSelected) { if (!isTextSelected) {
@ -410,11 +430,11 @@ function openTaskDetail() {
opacity: 1; opacity: 1;
} }
} }
.favorite:focus { .favorite:focus {
opacity: 1; opacity: 1;
} }
:deep(.fancycheckbox) { :deep(.fancycheckbox) {
height: 18px; height: 18px;
padding-top: 0; padding-top: 0;

View File

@ -95,6 +95,7 @@
:can-mark-as-done="canWrite || isSavedFilter(project)" :can-mark-as-done="canWrite || isSavedFilter(project)"
:the-task="t" :the-task="t"
@taskUpdated="updateTasks" @taskUpdated="updateTasks"
:all-tasks="allTasks"
> >
<template v-if="canWrite"> <template v-if="canWrite">
<span class="icon handle"> <span class="icon handle">
@ -120,7 +121,7 @@ export default { name: 'List' }
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, nextTick, onMounted} from 'vue' import {ref, computed, nextTick, onMounted, watch} from 'vue'
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import {useRoute, useRouter} from 'vue-router' import {useRoute, useRouter} from 'vue-router'
@ -160,7 +161,7 @@ const DRAG_OPTIONS = {
} as const } as const
const { const {
tasks, tasks: allTasks,
loading, loading,
totalPages, totalPages,
currentPage, currentPage,
@ -170,6 +171,13 @@ const {
sortByParam, sortByParam,
} = useTaskList(() => projectId, {position: 'asc' }) } = useTaskList(() => projectId, {position: 'asc' })
const tasks = ref<ITask[]>([])
watch(
allTasks,
() => {
tasks.value = [...allTasks.value].filter(t => typeof t.relatedTasks?.parenttask === 'undefined')
},
)
const isAlphabeticalSorting = computed(() => { const isAlphabeticalSorting = computed(() => {
return params.value.sort_by.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined return params.value.sort_by.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined