Improve backlog drag targets and persistent handle visibility

This commit is contained in:
OpenClaw Bot 2026-02-19 23:03:49 -06:00
parent 957907c87a
commit c5cf7703b8
2 changed files with 45 additions and 21 deletions

View File

@ -33,7 +33,7 @@
"projectId": "2", "projectId": "2",
"sprintId": "sprint-1", "sprintId": "sprint-1",
"createdAt": "2026-02-18T17:01:23.109Z", "createdAt": "2026-02-18T17:01:23.109Z",
"updatedAt": "2026-02-18T17:01:23.109Z", "updatedAt": "2026-02-20T05:01:04.207Z",
"comments": [ "comments": [
{ {
"id": "c1", "id": "c1",
@ -57,7 +57,6 @@
{ {
"id": "2", "id": "2",
"title": "MoodWeave App Idea - UPDATED", "title": "MoodWeave App Idea - UPDATED",
"sprintId": "1771551323429",
"projectId": "1", "projectId": "1",
"status": "backlog", "status": "backlog",
"priority": "high", "priority": "high",
@ -69,7 +68,7 @@
"OpenClaw iOS" "OpenClaw iOS"
], ],
"createdAt": "2026-02-18T17:01:23.109Z", "createdAt": "2026-02-18T17:01:23.109Z",
"updatedAt": "2026-02-20T02:28:23.700Z" "updatedAt": "2026-02-20T05:02:47.264Z"
}, },
{ {
"id": "3", "id": "3",
@ -323,7 +322,7 @@
"priority": "low", "priority": "low",
"projectId": "3", "projectId": "3",
"createdAt": "2026-02-18T17:01:23.109Z", "createdAt": "2026-02-18T17:01:23.109Z",
"updatedAt": "2026-02-18T17:01:23.109Z", "updatedAt": "2026-02-20T05:02:42.002Z",
"comments": [ "comments": [
{ {
"id": "c52", "id": "c52",
@ -472,7 +471,7 @@
] ]
} }
], ],
"lastUpdated": 1771556223745, "lastUpdated": 1771563767290,
"sprints": [ "sprints": [
{ {
"name": "Sprint 1", "name": "Sprint 1",
@ -506,4 +505,4 @@
"createdAt": "2026-02-20T01:37:45.241Z" "createdAt": "2026-02-20T01:37:45.241Z"
} }
] ]
} }

View File

@ -1,12 +1,13 @@
"use client" "use client"
import { useState } from "react" import { useState, type ReactNode } from "react"
import { import {
DndContext, DndContext,
DragEndEvent, DragEndEvent,
DragOverlay, DragOverlay,
DragStartEvent, DragStartEvent,
PointerSensor, PointerSensor,
useDroppable,
useSensor, useSensor,
useSensors, useSensors,
closestCorners, closestCorners,
@ -17,8 +18,8 @@ import {
useSortable, useSortable,
} from "@dnd-kit/sortable" } from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities" import { CSS } from "@dnd-kit/utilities"
import { useTaskStore, Task, Sprint } from "@/stores/useTaskStore" import { useTaskStore, Task } from "@/stores/useTaskStore"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Plus, GripVertical, ChevronDown, ChevronRight, Calendar } from "lucide-react" import { Plus, GripVertical, ChevronDown, ChevronRight, Calendar } from "lucide-react"
@ -66,13 +67,15 @@ function SortableTaskRow({
<div <div
ref={setNodeRef} ref={setNodeRef}
style={style} style={style}
className="flex items-center gap-3 p-3 bg-slate-800/50 border border-slate-700/50 rounded-lg hover:border-slate-600 cursor-pointer group" className="flex items-center gap-3 p-3 bg-slate-800/50 border border-slate-700/50 rounded-lg hover:border-slate-600 cursor-pointer"
onClick={onClick} onClick={onClick}
> >
<div <div
{...attributes} {...attributes}
{...listeners} {...listeners}
className="opacity-0 group-hover:opacity-100 transition-opacity cursor-grab active:cursor-grabbing" className="shrink-0 h-8 w-6 rounded border border-slate-700/70 bg-slate-900/70 flex items-center justify-center cursor-grab active:cursor-grabbing text-slate-400 hover:text-slate-200"
aria-label="Drag task"
title="Drag task"
> >
<GripVertical className="w-4 h-4 text-slate-500" /> <GripVertical className="w-4 h-4 text-slate-500" />
</div> </div>
@ -106,6 +109,19 @@ function DragOverlayItem({ task }: { task: Task }) {
) )
} }
function SectionDropZone({ id, children }: { id: string; children: ReactNode }) {
const { isOver, setNodeRef } = useDroppable({ id })
return (
<div
ref={setNodeRef}
className={`rounded-lg transition-colors ${isOver ? "ring-1 ring-blue-500/60 bg-blue-500/5" : ""}`}
>
{children}
</div>
)
}
// Collapsible Section // Collapsible Section
function TaskSection({ function TaskSection({
title, title,
@ -242,14 +258,23 @@ export function BacklogView() {
const taskId = active.id as string const taskId = active.id as string
const overId = over.id as string const overId = over.id as string
const overTask = tasks.find((t) => t.id === overId)
const destinationId = overTask
? overTask.sprintId
? currentSprint && overTask.sprintId === currentSprint.id
? "current"
: `sprint-${overTask.sprintId}`
: "backlog"
: overId
// If dropped over a section header, move task to that section's sprint // If dropped over a section header, move task to that section's sprint
if (overId === "backlog") { if (destinationId === "backlog") {
updateTask(taskId, { sprintId: undefined }) updateTask(taskId, { sprintId: undefined })
} else if (overId === "current" && currentSprint) { } else if (destinationId === "current" && currentSprint) {
updateTask(taskId, { sprintId: currentSprint.id }) updateTask(taskId, { sprintId: currentSprint.id })
} else if (overId.startsWith("sprint-")) { } else if (destinationId.startsWith("sprint-")) {
const sprintId = overId.replace("sprint-", "") const sprintId = destinationId.replace("sprint-", "")
updateTask(taskId, { sprintId }) updateTask(taskId, { sprintId })
} }
} }
@ -283,7 +308,7 @@ export function BacklogView() {
> >
<div className="space-y-4"> <div className="space-y-4">
{/* Current Sprint Section */} {/* Current Sprint Section */}
<div id="current"> <SectionDropZone id="current">
<TaskSection <TaskSection
title={currentSprint?.name || "Current Sprint"} title={currentSprint?.name || "Current Sprint"}
tasks={currentSprintTasks} tasks={currentSprintTasks}
@ -305,7 +330,7 @@ export function BacklogView() {
: undefined : undefined
} }
/> />
</div> </SectionDropZone>
{/* Other Sprints Sections - ordered by start date */} {/* Other Sprints Sections - ordered by start date */}
{otherSprints {otherSprints
@ -314,7 +339,7 @@ export function BacklogView() {
const sprintTasks = tasks.filter((t) => t.sprintId === sprint.id) const sprintTasks = tasks.filter((t) => t.sprintId === sprint.id)
console.log(`Sprint ${sprint.name}: ${sprintTasks.length} tasks`, sprintTasks.map(t => t.title)) console.log(`Sprint ${sprint.name}: ${sprintTasks.length} tasks`, sprintTasks.map(t => t.title))
return ( return (
<div key={sprint.id} id={`sprint-${sprint.id}`}> <SectionDropZone key={sprint.id} id={`sprint-${sprint.id}`}>
<TaskSection <TaskSection
title={sprint.name} title={sprint.name}
tasks={sprintTasks} tasks={sprintTasks}
@ -332,7 +357,7 @@ export function BacklogView() {
status: sprint.status, status: sprint.status,
}} }}
/> />
</div> </SectionDropZone>
) )
})} })}
@ -391,7 +416,7 @@ export function BacklogView() {
)} )}
{/* Backlog Section */} {/* Backlog Section */}
<div id="backlog"> <SectionDropZone id="backlog">
<TaskSection <TaskSection
title="Backlog" title="Backlog"
tasks={backlogTasks} tasks={backlogTasks}
@ -399,7 +424,7 @@ export function BacklogView() {
onToggle={() => toggleSection("backlog")} onToggle={() => toggleSection("backlog")}
onTaskClick={(task) => selectTask(task.id)} onTaskClick={(task) => selectTask(task.id)}
/> />
</div> </SectionDropZone>
</div> </div>
<DragOverlay> <DragOverlay>