Allow dragging tickets between OPEN, WIP, and DONE on the project board.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-27 23:29:01 +01:00
co-authored by Cursor
parent 34ab3b9c5b
commit f97856b8a6
2 changed files with 141 additions and 12 deletions
+92 -7
View File
@@ -53,6 +53,15 @@
let editTimeEnd = $state('')
let tokenName = $state('tui')
let tokenScope = $state('write')
let dragTicketId = $state('')
let dragOverStatus = $state('')
let suppressTicketClick = false
const boardStatuses = [
['todo', 'OPEN'],
['in_progress', 'WIP'],
['done', 'DONE'],
]
function desiredHash() {
return buildHash({
@@ -591,6 +600,63 @@
return tickets.filter((t) => t.status === status)
}
async function setTicketStatus(ticket, status) {
if (!ticket || ticket.status === status) return
error = ''
const prev = ticket.status
tickets = tickets.map((t) => (t.id === ticket.id ? { ...t, status } : t))
try {
await api.patchTicket(projectId, ticket.id, { status })
if (selected?.id === ticket.id) {
selected = { ...selected, status }
editStatus = status
}
} catch (e) {
tickets = tickets.map((t) => (t.id === ticket.id ? { ...t, status: prev } : t))
error = e.message
}
}
function onTicketDragStart(e, ticket) {
dragTicketId = ticket.id
e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/plain', ticket.id)
}
function onTicketDragEnd() {
dragTicketId = ''
dragOverStatus = ''
// Click fires after dragend on some browsers; suppress open-once.
suppressTicketClick = true
setTimeout(() => {
suppressTicketClick = false
}, 0)
}
function onColumnDragOver(e, status) {
e.preventDefault()
e.dataTransfer.dropEffect = 'move'
dragOverStatus = status
}
function onColumnDragLeave(e, status) {
if (dragOverStatus === status) dragOverStatus = ''
}
async function onColumnDrop(e, status) {
e.preventDefault()
const id = e.dataTransfer.getData('text/plain') || dragTicketId
dragTicketId = ''
dragOverStatus = ''
const ticket = tickets.find((t) => t.id === id)
if (ticket) await setTicketStatus(ticket, status)
}
function onBoardTicketClick(t) {
if (suppressTicketClick) return
openTicket(t)
}
async function copyText(text) {
try {
await navigator.clipboard.writeText(text)
@@ -775,15 +841,34 @@
</div>
</div>
<div class="grid cols-3">
{#each [['todo', 'OPEN'], ['in_progress', 'WIP'], ['done', 'DONE']] as [st, label]}
<div class="stack">
{#each boardStatuses as [st, label]}
<div
class="board-col stack"
class:drag-over={dragOverStatus === st}
ondragover={(e) => onColumnDragOver(e, st)}
ondragleave={(e) => onColumnDragLeave(e, st)}
ondrop={(e) => onColumnDrop(e, st)}
role="list"
aria-label={label}
>
<h3>{label}</h3>
{#each byStatus(st) as t}
<button class="ticket" onclick={() => openTicket(t)}>
<span class="badge {t.status}">{label}</span>
<div>{t.title}</div>
{#if t.externalId}<div class="muted">{t.externalId}</div>{/if}
</button>
<div
class="ticket"
class:dragging={dragTicketId === t.id}
draggable="true"
role="listitem"
ondragstart={(e) => onTicketDragStart(e, t)}
ondragend={onTicketDragEnd}
>
<button type="button" class="ticket-open" onclick={() => onBoardTicketClick(t)}>
<div>{t.title}</div>
{#if t.externalId}<div class="muted">{t.externalId}</div>{/if}
</button>
<span class="drag-handle" title="Ziehen" aria-hidden="true">⠿</span>
</div>
{:else}
<div class="board-empty muted">Drop hier</div>
{/each}
</div>
{/each}