feat: Parent-Child TaskFlow — Delegated durch sichtbare Child-Tasks ersetzt
- Delegated State aus Board, Entities, DTOs, Frontend-Spalten und Tests entfernt - Parent-Tasks bleiben InProgress waehrend delegierter Agentenarbeit - Child-Tasks laufen sichtbar mit normalen States und parentTaskId - Doku: README, Phase 3, Changelog, Controller-Kommentare angepasst - openclaw-task-board-flow.md als Referenzdoku hinzugefuegt - 73/73 Backend-Tests gruen, Frontend-Build gruen
This commit is contained in:
@@ -3,13 +3,13 @@
|
||||
* TaskBoardView – Linear-style Kanban Board
|
||||
* Galaxy/Dashboard V2 styled edition.
|
||||
*
|
||||
* 6 columns: Offen, In Bearbeitung, Delegiert, Review, Blockiert, Erledigt
|
||||
* 5 columns: Offen, In Bearbeitung, Review, Blockiert, Erledigt
|
||||
* HTML5 Drag & Drop (no external lib)
|
||||
*
|
||||
* Agent-Workflow Features:
|
||||
* - Agent-Tasks have a 🤖 badge
|
||||
* - ExpectedFrom field shows who is expected to act next
|
||||
* - Stale-task warning banner at top (InProgress/Delegated > 2h)
|
||||
* - Stale-task warning banner at top (InProgress > 2h)
|
||||
* - Waiting section for Iris overview
|
||||
*/
|
||||
import { computed, onBeforeUnmount, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
@@ -157,7 +157,6 @@ function statusTone(state: string): string {
|
||||
case 'done': return 'is-done'
|
||||
case 'blocked': return 'is-blocked'
|
||||
case 'review': return 'is-review'
|
||||
case 'delegated': return 'is-delegated'
|
||||
case 'in progress': return 'is-progress'
|
||||
default: return 'is-backlog'
|
||||
}
|
||||
@@ -183,7 +182,6 @@ function flattenBoard() {
|
||||
return [
|
||||
...taskStore.board.offen,
|
||||
...taskStore.board.inProgress,
|
||||
...taskStore.board.delegated,
|
||||
...taskStore.board.review,
|
||||
...taskStore.board.blocked,
|
||||
...taskStore.board.done,
|
||||
@@ -250,8 +248,45 @@ function relativeTime(date?: string | null): string {
|
||||
return `vor ${days} d`
|
||||
}
|
||||
|
||||
function childStatusSummary(taskId: string): string {
|
||||
const children = allBoardTasks.value.filter(task => task.parentTaskId === taskId)
|
||||
if (!children.length) return ''
|
||||
|
||||
const counts = {
|
||||
inProgress: children.filter(task => task.state === 'In progress').length,
|
||||
review: children.filter(task => task.state === 'Review').length,
|
||||
blocked: children.filter(task => task.state === 'Blocked').length,
|
||||
done: children.filter(task => task.state === 'Done').length,
|
||||
}
|
||||
|
||||
const parts = [] as string[]
|
||||
if (counts.inProgress) parts.push(`${counts.inProgress} in Arbeit`)
|
||||
if (counts.review) parts.push(`${counts.review} im Review`)
|
||||
if (counts.blocked) parts.push(`${counts.blocked} blockiert`)
|
||||
if (counts.done) parts.push(`${counts.done} erledigt`)
|
||||
return parts.length ? `Child-Tasks: ${parts.join(' · ')}` : `Child-Tasks: ${children.length}`
|
||||
}
|
||||
|
||||
function activityHint(task: BoardTask): string {
|
||||
return task.lastActivityMessage?.trim() || (task.expectedFrom ? `Wartet auf ${expectedFromLabel(task.expectedFrom)}` : 'Noch kein relevanter Progress-Status')
|
||||
return task.lastActivityMessage?.trim()
|
||||
|| childStatusSummary(task.id)
|
||||
|| (task.expectedFrom ? `Wartet auf ${expectedFromLabel(task.expectedFrom)}` : 'Noch kein relevanter Progress-Status')
|
||||
}
|
||||
|
||||
function hasChildTasks(taskId: string): boolean {
|
||||
return allBoardTasks.value.some(task => task.parentTaskId === taskId)
|
||||
}
|
||||
|
||||
function assigneeLabel(assignedTo: string | null | undefined): string {
|
||||
return expectedFromLabel(assignedTo)
|
||||
}
|
||||
|
||||
function assigneeClass(assignedTo: string | null | undefined): string {
|
||||
if (!assignedTo) return ''
|
||||
const lower = assignedTo.toLowerCase()
|
||||
if (lower === 'iris') return 'assignee-iris'
|
||||
if (lower === 'bao') return 'assignee-bao'
|
||||
return 'assignee-agent'
|
||||
}
|
||||
|
||||
/* ── Task Navigation ───────────────────────────── */
|
||||
@@ -399,7 +434,7 @@ onUnmounted(() => {
|
||||
<!-- Stale Warning Banner -->
|
||||
<div v-if="staleCount > 0" class="stale-banner">
|
||||
<AlertTriangle :size="14" />
|
||||
<span><strong>{{ staleCount }} Task(s)</strong> sind stale (InBearbeitung/Delegiert > 2h ohne Update).</span>
|
||||
<span><strong>{{ staleCount }} Task(s)</strong> sind stale (In Bearbeitung > 2h ohne Update).</span>
|
||||
<button class="stale-dismiss" @click="showIrisPanel = true">Ansehen</button>
|
||||
</div>
|
||||
|
||||
@@ -514,14 +549,14 @@ onUnmounted(() => {
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
:class="assigneeClass(task.assignedTo)"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
{{ assigneeLabel(task.assignedTo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.offen.length" class="empty-col">Keine Aufgaben</div>
|
||||
@@ -563,69 +598,20 @@ onUnmounted(() => {
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
:class="assigneeClass(task.assignedTo)"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
{{ assigneeLabel(task.assignedTo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.inProgress.length" class="empty-col">Keine Aufgaben</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col"
|
||||
:class="{ 'drag-over': dragOverColumn === 'delegated' }"
|
||||
@dragover="onDragOver($event, 'delegated')"
|
||||
@dragleave="onDragLeave"
|
||||
@drop="onDrop($event, 'delegated')"
|
||||
>
|
||||
<div class="col-header">
|
||||
<span class="col-icon" style="background: var(--a-purple); box-shadow: 0 0 0 2px rgba(181,87,246,.25);"></span>
|
||||
<span class="col-name">Delegiert</span>
|
||||
<span class="col-count">{{ taskStore.board.delegated.length }}</span>
|
||||
</div>
|
||||
<div class="col-cards">
|
||||
<button
|
||||
v-for="task in taskStore.board.delegated"
|
||||
:key="task.id"
|
||||
type="button"
|
||||
class="card"
|
||||
:class="{ 'card-agent': task.isAgentTask }"
|
||||
draggable="true"
|
||||
@click="handleCardClick($event, task.id)"
|
||||
@dragstart="onDragStart($event, task.id)"
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
<div class="card-top">
|
||||
<span class="prio-badge" :style="{ color: priorityColor(task.priority), borderColor: priorityColor(task.priority) }">
|
||||
{{ priorityLabel(task.priority) }}
|
||||
</span>
|
||||
<span v-if="task.isAgentTask" class="agent-badge" title="Agent-Task">🤖</span>
|
||||
<span v-if="task.expectedFrom" class="expected-badge" :title="'Erwartet: ' + task.expectedFrom">
|
||||
⏳ {{ task.expectedFrom }}
|
||||
</span>
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.delegated.length" class="empty-col">Keine delegierten Aufgaben</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col"
|
||||
:class="{ 'drag-over': dragOverColumn === 'review' }"
|
||||
@@ -661,14 +647,14 @@ onUnmounted(() => {
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
:class="assigneeClass(task.assignedTo)"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
{{ assigneeLabel(task.assignedTo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.review.length" class="empty-col">Keine Aufgaben</div>
|
||||
@@ -705,14 +691,14 @@ onUnmounted(() => {
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
:class="assigneeClass(task.assignedTo)"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
{{ assigneeLabel(task.assignedTo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.done.length" class="empty-col">Keine Aufgaben</div>
|
||||
@@ -754,13 +740,14 @@ onUnmounted(() => {
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
:class="assigneeClass(task.assignedTo)"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
{{ assigneeLabel(task.assignedTo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.blocked.length" class="empty-col">Keine Blockierer</div>
|
||||
@@ -849,11 +836,12 @@ onUnmounted(() => {
|
||||
<span>#{{ selectedTask.id.slice(0, 8) }}</span>
|
||||
<span v-if="selectedTask.isAgentTask" class="meta-agent-tag">🤖 Agent-Task</span>
|
||||
<span v-if="selectedTask.expectedFrom" class="meta-expected">⏳ Erwartet: {{ selectedTask.expectedFrom }}</span>
|
||||
<span v-if="selectedTask.parentTaskId" class="meta-expected">↳ Child-Task</span>
|
||||
<span><Clock3 :size="13" /> Aktualisiert {{ formatDate(selectedTask.updatedAt, true) }}</span>
|
||||
<span><CalendarDays :size="13" /> Erstellt {{ formatDate(selectedTask.createdAt) }}</span>
|
||||
<span v-if="selectedTask.isAgentTask"><MessageSquareText :size="13" /> Letzter Status {{ relativeTime(selectedTask.lastActivityAt ?? selectedTask.updatedAt) }}</span>
|
||||
</div>
|
||||
<div v-if="selectedTask.isAgentTask" class="detail-progress-banner">
|
||||
<div v-if="selectedTask.isAgentTask || hasChildTasks(selectedTask.id)" class="detail-progress-banner">
|
||||
<strong>Letzter Fortschritt:</strong> {{ activityHint(selectedTask) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -914,7 +902,6 @@ onUnmounted(() => {
|
||||
>
|
||||
<option value="Backlog">Offen</option>
|
||||
<option value="In progress">In Bearbeitung</option>
|
||||
<option value="Delegated">Delegiert</option>
|
||||
<option value="Review">Review</option>
|
||||
<option value="Blocked">Blockiert</option>
|
||||
<option value="Done">Erledigt</option>
|
||||
@@ -970,6 +957,10 @@ onUnmounted(() => {
|
||||
<dt>Erwartet von</dt>
|
||||
<dd>{{ selectedTask.expectedFrom }}</dd>
|
||||
</div>
|
||||
<div v-if="selectedTask.parentTaskId">
|
||||
<dt>Task-Typ</dt>
|
||||
<dd>Sichtbare Child-Task</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -1078,6 +1069,7 @@ select:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.assignee { font-family: 'Manrope', sans-serif; font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; }
|
||||
.assignee-iris { background: rgba(147, 51, 234, .12); color: #c084fc; }
|
||||
.assignee-bao { background: rgba(59, 130, 246, .12); color: #60a5fa; }
|
||||
.assignee-agent { background: rgba(16, 185, 129, .12); color: #6ee7b7; }
|
||||
.card-title { font-size: 12.5px; font-weight: 600; color: var(--tx); line-height: 1.4; word-break: break-word; font-family: 'Manrope', sans-serif; }
|
||||
.card-preview { margin-top: 6px; font-size: 11px; line-height: 1.45; color: var(--tx-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.card-progress-hint { margin-top: 7px; font-size: 10.5px; color: var(--tx-2); line-height: 1.4; padding: 6px 8px; border-radius: 8px; background: rgba(124,108,255,.07); border: 1px solid rgba(124,108,255,.12); }
|
||||
@@ -1123,7 +1115,6 @@ select:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.detail-state-pill { width: fit-content; border-radius: 999px; padding: 5px 10px; font-size: 11px; font-weight: 700; letter-spacing: .03em; border: 1px solid transparent; }
|
||||
.detail-state-pill.is-backlog { color: #fde68a; background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.25); }
|
||||
.detail-state-pill.is-progress { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
||||
.detail-state-pill.is-delegated { color: #d8b4fe; background: rgba(168,85,247,.12); border-color: rgba(168,85,247,.25); }
|
||||
.detail-state-pill.is-review { color: #fdba74; background: rgba(249,115,22,.12); border-color: rgba(249,115,22,.25); }
|
||||
.detail-state-pill.is-blocked { color: #fda4af; background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.25); }
|
||||
.detail-state-pill.is-done { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
||||
|
||||
Reference in New Issue
Block a user