feat(list view): show subtasks nested
Resolves https://kolaente.dev/vikunja/frontend/issues/363
This commit is contained in:
parent
842e2c2811
commit
e41712647d
@ -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">, </template>
|
|
||||||
</template>
|
|
||||||
›
|
|
||||||
</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;
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user