feat: add collapsible subtasks in list view

- Add chevron toggle button for each task with subtasks
- Add global collapse/expand all button in filter toolbar
- Use provide/inject for global collapse state
- Smooth rotation animation on collapse toggle

Closes #126
This commit is contained in:
sjin.on.ca@gmail.com 2026-06-11 15:55:08 +01:00
parent 89ee1ef507
commit db1e51428b
2 changed files with 95 additions and 3 deletions

View File

@ -17,6 +17,14 @@
:project-id="projectId"
@update:modelValue="loadTasks()"
/>
<button
class="collapse-all-btn"
:class="{ 'is-collapsed': collapseAll }"
@click="toggleCollapseAll"
:title="collapseAll ? 'Expand all subtasks' : 'Collapse all subtasks'"
>
<Icon :icon="collapseAll ? 'chevron-right' : 'chevron-down'" />
</button>
</div>
</template>
@ -101,7 +109,7 @@
<script setup lang="ts">
import {ref, computed, nextTick, onMounted, onBeforeUnmount, watch, toRef} from 'vue'
import {ref, computed, nextTick, onMounted, onBeforeUnmount, watch, toRef, provide} from 'vue'
import draggable from 'zhyswan-vuedraggable'
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
@ -143,6 +151,14 @@ const ctaVisible = ref(false)
const drag = ref(false)
// Collapse all subtasks
const collapseAll = ref(false)
provide('collapseAll', collapseAll)
function toggleCollapseAll() {
collapseAll.value = !collapseAll.value
}
const {
tasks: allTasks,
loading,
@ -368,6 +384,31 @@ onBeforeUnmount(() => {
}
}
.collapse-all-btn {
display: flex;
align-items: center;
justify-content: center;
inline-size: 32px;
block-size: 32px;
border: 1px solid var(--grey-300);
background: var(--white);
color: var(--grey-500);
cursor: pointer;
border-radius: $radius;
transition: all 0.2s ease;
&:hover {
color: var(--grey-700);
background: var(--grey-100);
border-color: var(--grey-400);
}
&.is-collapsed {
color: var(--primary);
border-color: var(--primary);
}
}
.tasks {
padding: .5rem;
}

View File

@ -178,8 +178,17 @@
/>
</BaseButton>
<slot />
<button
v-if="hasSubtasks"
class="collapse-toggle"
:class="{ 'is-collapsed': isCollapsed }"
@click.stop="toggleCollapse"
:aria-label="isCollapsed ? 'Expand subtasks' : 'Collapse subtasks'"
>
<Icon icon="chevron-down" />
</button>
</div>
<template v-if="typeof task.relatedTasks?.subtask !== 'undefined'">
<template v-if="hasSubtasks && !isCollapsed">
<template v-for="subtask in task.relatedTasks.subtask">
<template v-if="getTaskById(subtask.id)">
<single-task-in-project
@ -197,7 +206,7 @@
</template>
<script setup lang="ts">
import {ref, watch, shallowReactive, onMounted, computed} from 'vue'
import {ref, watch, shallowReactive, onMounted, computed, inject, type Ref} from 'vue'
import {useI18n} from 'vue-i18n'
import TaskModel, {getHexColor} from '@/models/task'
@ -246,6 +255,24 @@ const props = withDefaults(defineProps<{
allTasks: () => [],
})
// Collapse state
const isCollapsed = ref(false)
const collapseAll = inject<Ref<boolean>>('collapseAll', ref(false))
// Watch for global collapse/expand all
watch(collapseAll, (newVal) => {
isCollapsed.value = newVal
})
const hasSubtasks = computed(() => {
return typeof props.theTask.relatedTasks?.subtask !== 'undefined' &&
props.theTask.relatedTasks.subtask.length > 0
})
function toggleCollapse() {
isCollapsed.value = !isCollapsed.value
}
const emit = defineEmits<{
'taskUpdated': [task: ITask],
}>()
@ -597,6 +624,30 @@ defineExpose({
margin-inline-start: 1.75rem;
}
.collapse-toggle {
display: flex;
align-items: center;
justify-content: center;
inline-size: 20px;
block-size: 20px;
border: none;
background: transparent;
color: var(--grey-400);
cursor: pointer;
border-radius: $radius;
transition: transform 0.2s ease, color 0.2s ease;
margin-inline-start: 0.5rem;
&:hover {
color: var(--grey-600);
background: var(--grey-100);
}
&.is-collapsed {
transform: rotate(-90deg);
}
}
:deep(.popup) {
border-radius: $radius;
background-color: var(--white);