Allow dragging tickets between OPEN, WIP, and DONE on the project board.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+92
-7
@@ -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}
|
||||
|
||||
+49
-5
@@ -350,20 +350,38 @@ code {
|
||||
.error { color: var(--danger); margin: 0.5rem 0; }
|
||||
.row { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||||
.stack { display: grid; gap: 0.6rem; }
|
||||
.board-col {
|
||||
min-height: 8rem;
|
||||
padding: 0.55rem;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed transparent;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
.board-col.drag-over {
|
||||
border-color: color-mix(in srgb, var(--accent) 70%, var(--line));
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
}
|
||||
.board-empty {
|
||||
padding: 0.75rem 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
text-align: center;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.ticket {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0.65rem 0.85rem;
|
||||
border-radius: 6px;
|
||||
padding: 0.55rem 0.7rem 0.4rem;
|
||||
background: color-mix(in srgb, var(--panel-2) 55%, transparent);
|
||||
cursor: pointer;
|
||||
cursor: grab;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
color: var(--ink);
|
||||
font-weight: 500;
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.ticket:hover {
|
||||
border: none;
|
||||
color: var(--ink);
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
color-mix(in srgb, var(--accent) 35%, transparent) 0%,
|
||||
@@ -371,6 +389,32 @@ code {
|
||||
color-mix(in srgb, var(--panel-2) 70%, transparent) 100%
|
||||
);
|
||||
}
|
||||
.ticket.dragging {
|
||||
opacity: 0.45;
|
||||
cursor: grabbing;
|
||||
}
|
||||
.ticket-open {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
width: 100%;
|
||||
padding: 0.05rem 0;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
font-weight: inherit;
|
||||
background: transparent;
|
||||
}
|
||||
.ticket-open:hover {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
}
|
||||
.drag-handle {
|
||||
justify-self: end;
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
letter-spacing: -0.08em;
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
|
||||
Reference in New Issue
Block a user