kin: KIN-127-frontend_dev
This commit is contained in:
parent
d552b8bd45
commit
484c9fc800
5 changed files with 487 additions and 15 deletions
|
|
@ -681,6 +681,71 @@ const manualEscalationTasks = computed(() => {
|
|||
)
|
||||
})
|
||||
|
||||
// Tree helpers
|
||||
const childrenMap = computed(() => {
|
||||
const map = new Map<string, Task[]>()
|
||||
for (const t of (project.value?.tasks || [])) {
|
||||
if (t.parent_task_id) {
|
||||
const arr = map.get(t.parent_task_id) || []
|
||||
arr.push(t)
|
||||
map.set(t.parent_task_id, arr)
|
||||
}
|
||||
}
|
||||
return map
|
||||
})
|
||||
|
||||
function taskDepth(task: Task): number {
|
||||
let depth = 0
|
||||
let current = task
|
||||
const visited = new Set<string>()
|
||||
while (current.parent_task_id && !visited.has(current.id)) {
|
||||
visited.add(current.id)
|
||||
const parent = (project.value?.tasks || []).find(t => t.id === current.parent_task_id)
|
||||
if (!parent) break
|
||||
current = parent
|
||||
depth++
|
||||
}
|
||||
return depth
|
||||
}
|
||||
|
||||
const expandedTasks = ref(new Set<string>())
|
||||
|
||||
function toggleExpand(taskId: string) {
|
||||
const next = new Set(expandedTasks.value)
|
||||
if (next.has(taskId)) next.delete(taskId)
|
||||
else next.add(taskId)
|
||||
expandedTasks.value = next
|
||||
}
|
||||
|
||||
function hasChildren(taskId: string): boolean {
|
||||
return (childrenMap.value.get(taskId)?.length || 0) > 0
|
||||
}
|
||||
|
||||
const rootFilteredTasks = computed(() => {
|
||||
const taskIds = new Set((project.value?.tasks || []).map(t => t.id))
|
||||
return filteredTasks.value.filter(t => {
|
||||
if (!t.parent_task_id) return true
|
||||
return !taskIds.has(t.parent_task_id)
|
||||
})
|
||||
})
|
||||
|
||||
const flattenedTasks = computed(() => {
|
||||
const result: Task[] = []
|
||||
function addWithChildren(task: Task) {
|
||||
result.push(task)
|
||||
if (expandedTasks.value.has(task.id)) {
|
||||
const children = childrenMap.value.get(task.id) || []
|
||||
for (const child of children) {
|
||||
addWithChildren(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const t of rootFilteredTasks.value) {
|
||||
addWithChildren(t)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
const filteredDecisions = computed(() => {
|
||||
if (!project.value) return []
|
||||
let decs = project.value.decisions
|
||||
|
|
@ -796,6 +861,7 @@ const KANBAN_COLUMNS = computed(() => [
|
|||
{ status: 'pending', label: t('projectView.kanban_pending'), headerClass: 'text-gray-400', bgClass: 'bg-gray-900/20' },
|
||||
{ status: 'in_progress', label: t('projectView.kanban_in_progress'), headerClass: 'text-blue-400', bgClass: 'bg-blue-950/20' },
|
||||
{ status: 'review', label: t('projectView.kanban_review'), headerClass: 'text-purple-400', bgClass: 'bg-purple-950/20' },
|
||||
{ status: 'revising', label: t('projectView.kanban_revising'), headerClass: 'text-orange-400', bgClass: 'bg-orange-950/20' },
|
||||
{ status: 'blocked', label: t('projectView.kanban_blocked'), headerClass: 'text-red-400', bgClass: 'bg-red-950/20' },
|
||||
{ status: 'done', label: t('projectView.kanban_done'), headerClass: 'text-green-400', bgClass: 'bg-green-950/20' },
|
||||
])
|
||||
|
|
@ -1127,13 +1193,20 @@ async function addDecision() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredTasks.length === 0" class="text-gray-600 text-sm">{{ t('projectView.no_tasks') }}</div>
|
||||
<div v-if="flattenedTasks.length === 0" class="text-gray-600 text-sm">{{ t('projectView.no_tasks') }}</div>
|
||||
<div v-else class="space-y-1">
|
||||
<router-link v-for="t in filteredTasks" :key="t.id"
|
||||
<div v-for="t in flattenedTasks" :key="t.id" :style="{ paddingLeft: taskDepth(t) * 24 + 'px' }">
|
||||
<router-link
|
||||
:to="{ path: `/task/${t.id}`, query: selectedStatuses.length ? { back_status: selectedStatuses.join(',') } : undefined }"
|
||||
class="flex flex-col gap-0.5 px-3 py-2 border border-gray-800 rounded text-sm hover:border-gray-600 no-underline block transition-colors">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<button v-if="hasChildren(t.id)" @click.prevent="toggleExpand(t.id)"
|
||||
data-testid="task-toggle-children"
|
||||
class="text-gray-500 hover:text-gray-300 w-4 shrink-0">
|
||||
{{ expandedTasks.has(t.id) ? '▼' : '▶' }}
|
||||
</button>
|
||||
<span v-else class="w-4 shrink-0"></span>
|
||||
<span class="text-gray-500 shrink-0 w-24">{{ t.id }}</span>
|
||||
<Badge :text="t.status" :color="taskStatusColor(t.status)" />
|
||||
<Badge v-if="t.category" :text="t.category" :color="CATEGORY_COLORS[t.category] || 'gray'" />
|
||||
|
|
@ -1185,6 +1258,7 @@ async function addDecision() {
|
|||
</div>
|
||||
<div v-if="t.status === 'blocked' && t.blocked_reason" class="text-xs text-red-400 truncate">{{ t.blocked_reason }}</div>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue