Skip to content

Commit 61e4398

Browse files
Move task delete to config modal, remove bulk selection
Replace bulk selection UI on /tasks view with delete button in the task config modal (opened via settings icon on task cards). Delete confirmation includes checkbox to optionally preserve the linked worktree.
1 parent a18e9f2 commit 61e4398

4 files changed

Lines changed: 111 additions & 227 deletions

File tree

src/components/kanban/selection-context.tsx

Lines changed: 0 additions & 74 deletions
This file was deleted.

src/components/kanban/task-card.tsx

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/eleme
77
import { attachClosestEdge, extractClosestEdge, type Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'
88
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'
99
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
10-
import { Checkbox } from '@/components/ui/checkbox'
11-
import { useSelection } from './selection-context'
1210
import { useDrag } from './drag-context'
1311
import type { Task } from '@/types'
1412
import { cn } from '@/lib/utils'
@@ -22,9 +20,7 @@ interface TaskCardProps {
2220
}
2321

2422
export function TaskCard({ task, isDragPreview }: TaskCardProps) {
25-
const { selectMode, isSelected, toggle } = useSelection()
2623
const { setActiveTask } = useDrag()
27-
const selected = isSelected(task.id)
2824
const navigate = useNavigate()
2925

3026
const ref = useRef<HTMLDivElement>(null)
@@ -38,7 +34,7 @@ export function TaskCard({ task, isDragPreview }: TaskCardProps) {
3834

3935
useEffect(() => {
4036
const el = ref.current
41-
if (!el || selectMode || isDragPreview) return
37+
if (!el || isDragPreview) return
4238

4339
return combine(
4440
draggable({
@@ -102,19 +98,13 @@ export function TaskCard({ task, isDragPreview }: TaskCardProps) {
10298
},
10399
})
104100
)
105-
}, [task, selectMode, isDragPreview, setActiveTask])
101+
}, [task, isDragPreview, setActiveTask])
106102

107103
const handlePointerDown = () => {
108104
hasDragged.current = false
109105
}
110106

111-
const handleClick = (e: React.MouseEvent) => {
112-
if (selectMode) {
113-
e.preventDefault()
114-
toggle(task.id)
115-
return
116-
}
117-
107+
const handleClick = () => {
118108
// Only navigate if we didn't drag
119109
if (!hasDragged.current) {
120110
navigate({ to: '/tasks/$taskId', params: { taskId: task.id } })
@@ -128,10 +118,8 @@ export function TaskCard({ task, isDragPreview }: TaskCardProps) {
128118
onPointerDown={handlePointerDown}
129119
onClick={handleClick}
130120
className={cn(
131-
'transition-shadow hover:shadow-md relative',
132-
selectMode ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing',
133-
isDragging && 'opacity-50',
134-
selected && 'ring-2 ring-primary'
121+
'transition-shadow hover:shadow-md relative cursor-grab active:cursor-grabbing',
122+
isDragging && 'opacity-50'
135123
)}
136124
>
137125
{/* Drop indicator line */}
@@ -145,24 +133,11 @@ export function TaskCard({ task, isDragPreview }: TaskCardProps) {
145133
/>
146134
)}
147135

148-
{/* Checkbox in top-right - only in select mode */}
149-
{selectMode && (
150-
<div
151-
className="absolute right-2 top-2 z-10 cursor-pointer"
152-
onClick={(e) => {
153-
e.stopPropagation()
154-
toggle(task.id)
155-
}}
156-
>
157-
<Checkbox checked={selected} className="cursor-pointer pointer-events-none" />
158-
</div>
159-
)}
160-
161-
<CardHeader className={cn('p-3 pb-1 flex flex-row items-start justify-between gap-2', selectMode && 'pr-8')}>
136+
<CardHeader className="p-3 pb-1 flex flex-row items-start justify-between gap-2">
162137
<CardTitle className="text-sm font-medium leading-tight flex-1">
163138
{task.title}
164139
</CardTitle>
165-
{!selectMode && !isDragPreview && (
140+
{!isDragPreview && (
166141
<button
167142
type="button"
168143
className="shrink-0 p-0.5 -m-0.5 rounded hover:bg-muted transition-colors cursor-pointer"

src/components/task-config-modal.tsx

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import { useState, useEffect } from 'react'
22
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
3+
import {
4+
AlertDialog,
5+
AlertDialogAction,
6+
AlertDialogCancel,
7+
AlertDialogContent,
8+
AlertDialogDescription,
9+
AlertDialogFooter,
10+
AlertDialogHeader,
11+
AlertDialogTitle,
12+
AlertDialogTrigger,
13+
} from '@/components/ui/alert-dialog'
314
import { Field, FieldGroup, FieldLabel } from '@/components/ui/field'
415
import { Input } from '@/components/ui/input'
516
import { DescriptionTextarea } from '@/components/ui/description-textarea'
17+
import { Checkbox } from '@/components/ui/checkbox'
618
import { Button } from '@/components/ui/button'
7-
import { useUpdateTask } from '@/hooks/use-tasks'
19+
import { useUpdateTask, useDeleteTask } from '@/hooks/use-tasks'
820
import type { Task } from '@/types'
921

1022
interface TaskConfigModalProps {
@@ -20,8 +32,10 @@ function parseLinearUrl(url: string): string | null {
2032

2133
export function TaskConfigModal({ task, open, onOpenChange }: TaskConfigModalProps) {
2234
const updateTask = useUpdateTask()
35+
const deleteTask = useDeleteTask()
2336

2437
const [title, setTitle] = useState(task.title)
38+
const [deleteLinkedWorktree, setDeleteLinkedWorktree] = useState(true)
2539
const [description, setDescription] = useState(task.description || '')
2640
const [prUrl, setPrUrl] = useState(task.prUrl || '')
2741
const [linearUrl, setLinearUrl] = useState(task.linearTicketUrl || '')
@@ -33,6 +47,7 @@ export function TaskConfigModal({ task, open, onOpenChange }: TaskConfigModalPro
3347
setDescription(task.description || '')
3448
setPrUrl(task.prUrl || '')
3549
setLinearUrl(task.linearTicketUrl || '')
50+
setDeleteLinkedWorktree(true)
3651
}
3752
}, [open, task])
3853

@@ -76,6 +91,17 @@ export function TaskConfigModal({ task, open, onOpenChange }: TaskConfigModalPro
7691
}
7792
}
7893

94+
const handleDelete = () => {
95+
deleteTask.mutate(
96+
{ taskId: task.id, deleteLinkedWorktree },
97+
{
98+
onSuccess: () => {
99+
onOpenChange(false)
100+
},
101+
}
102+
)
103+
}
104+
79105
return (
80106
<Dialog open={open} onOpenChange={onOpenChange}>
81107
<DialogContent className="sm:max-w-md" onKeyDown={handleKeyDown}>
@@ -120,13 +146,50 @@ export function TaskConfigModal({ task, open, onOpenChange }: TaskConfigModalPro
120146
/>
121147
</Field>
122148
</FieldGroup>
123-
<DialogFooter className="mt-4">
124-
<Button variant="outline" onClick={() => onOpenChange(false)}>
125-
Cancel
126-
</Button>
127-
<Button onClick={handleSave} disabled={!title.trim()}>
128-
Save
129-
</Button>
149+
<DialogFooter className="mt-4 flex-row justify-between sm:justify-between">
150+
<AlertDialog>
151+
<AlertDialogTrigger
152+
render={<Button variant="ghost" className="text-destructive hover:text-destructive hover:bg-destructive/10" disabled={deleteTask.isPending} />}
153+
>
154+
{deleteTask.isPending ? 'Deleting...' : 'Delete Task'}
155+
</AlertDialogTrigger>
156+
<AlertDialogContent>
157+
<AlertDialogHeader>
158+
<AlertDialogTitle>Delete Task</AlertDialogTitle>
159+
<AlertDialogDescription>
160+
This will permanently delete this task. This action cannot be undone.
161+
</AlertDialogDescription>
162+
</AlertDialogHeader>
163+
<div className="flex items-center gap-2 py-2">
164+
<Checkbox
165+
id="delete-worktree"
166+
checked={deleteLinkedWorktree}
167+
onCheckedChange={(checked) => setDeleteLinkedWorktree(checked === true)}
168+
/>
169+
<label htmlFor="delete-worktree" className="text-sm cursor-pointer">
170+
Also delete linked worktree
171+
</label>
172+
</div>
173+
<AlertDialogFooter>
174+
<AlertDialogCancel>Cancel</AlertDialogCancel>
175+
<AlertDialogAction
176+
onClick={handleDelete}
177+
variant="destructive"
178+
disabled={deleteTask.isPending}
179+
>
180+
{deleteTask.isPending ? 'Deleting...' : 'Delete'}
181+
</AlertDialogAction>
182+
</AlertDialogFooter>
183+
</AlertDialogContent>
184+
</AlertDialog>
185+
<div className="flex gap-2">
186+
<Button variant="outline" onClick={() => onOpenChange(false)}>
187+
Cancel
188+
</Button>
189+
<Button onClick={handleSave} disabled={!title.trim()}>
190+
Save
191+
</Button>
192+
</div>
130193
</DialogFooter>
131194
</DialogContent>
132195
</Dialog>

0 commit comments

Comments
 (0)