diff --git a/web/src/App.svelte b/web/src/App.svelte index 87425e0..2d5b5b9 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -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 @@
- {#each [['todo', 'OPEN'], ['in_progress', 'WIP'], ['done', 'DONE']] as [st, label]} -
+ {#each boardStatuses as [st, label]} +
onColumnDragOver(e, st)} + ondragleave={(e) => onColumnDragLeave(e, st)} + ondrop={(e) => onColumnDrop(e, st)} + role="list" + aria-label={label} + >

{label}

{#each byStatus(st) as t} - +
onTicketDragStart(e, t)} + ondragend={onTicketDragEnd} + > + + +
+ {:else} +
Drop hier
{/each}
{/each} diff --git a/web/src/app.css b/web/src/app.css index 10c1433..cc40c47 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -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;