From 5828d1796db87dcc4b52a022d26d511d1fe4aa39 Mon Sep 17 00:00:00 2001 From: perco Date: Tue, 19 May 2026 17:49:26 +0200 Subject: [PATCH] fix(planka): endpoints v2, tri intra-liste, drag&drop robuste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create_card: POST /api/lists/{id}/cards + type=story (Planka v2 requis) - create_list: type=active requis - Drag & drop: zones pk-drop-before avant chaque carte pour tri intra-liste - moveCard(): position calculée (avg prev/next) + gestion même liste - Suppression variable dragCardId globale → dataTransfer propre Co-Authored-By: Claude Sonnet 4.6 --- modules/planka/api.php | 21 +++++----- modules/planka/assets/planka.css | 14 +++++++ planka.php | 69 +++++++++++++++++++++++--------- 3 files changed, 75 insertions(+), 29 deletions(-) diff --git a/modules/planka/api.php b/modules/planka/api.php index 06dce12..637ed1f 100644 --- a/modules/planka/api.php +++ b/modules/planka/api.php @@ -99,25 +99,28 @@ if ($action === 'set_active_board') { if ($action === 'create_list') { $board_id = $_GET['board_id'] ?? null; if (!$board_id) tErr('board_id manquant'); $name = trim($_GET['name'] ?? ''); if (!$name) tErr('name manquant'); - $position = (int) ($_GET['position'] ?? 65535); + $position = (float) ($_GET['position'] ?? 65535); $resp = planka_api('POST', '/api/boards/' . $board_id . '/lists', [ 'name' => $name, 'position' => $position, + 'type' => 'active', ], $token); - if (empty($resp['item'])) tErr($resp['message'] ?? 'Planka n\'a pas créé la liste', 502); + if (empty($resp['item'])) tErr($resp['message'] ?? ($resp['problems'][0] ?? 'Erreur Planka'), 502); tOk($resp['item']); } if ($action === 'create_card') { - $list_id = $_GET['list_id'] ?? null; if (!$list_id) tErr('list_id manquant'); - $board_id = $_GET['board_id'] ?? null; if (!$board_id) tErr('board_id manquant'); - $name = trim($_GET['name'] ?? ''); if (!$name) tErr('name manquant'); - $resp = planka_api('POST', '/api/boards/' . $board_id . '/cards', [ - 'listId' => $list_id, + $list_id = $_GET['list_id'] ?? null; if (!$list_id) tErr('list_id manquant'); + $name = trim($_GET['name'] ?? ''); if (!$name) tErr('name manquant'); + $pos = (float) ($_GET['position'] ?? 65535); + // Planka v2+: POST /api/lists/{listId}/cards with type required + $resp = planka_api('POST', '/api/lists/' . $list_id . '/cards', [ 'name' => $name, - 'position' => 65535, + 'position' => $pos, + 'type' => 'story', ], $token); - tOk($resp['item'] ?? []); + if (empty($resp['item'])) tErr($resp['message'] ?? ($resp['problems'][0] ?? 'Erreur Planka'), 502); + tOk($resp['item']); } if ($action === 'update_card') { diff --git a/modules/planka/assets/planka.css b/modules/planka/assets/planka.css index eb8ab33..24d4cc2 100644 --- a/modules/planka/assets/planka.css +++ b/modules/planka/assets/planka.css @@ -488,3 +488,17 @@ background: var(--bg-panel, #f3f4f6); color: var(--primary, #4361ee); } + +.pk-drop-before { + height: 4px; + border-radius: 4px; + transition: height .12s, background .12s; + margin: -2px 0; +} +.pk-drop-before.drop-before-active { + height: 28px; + background: rgba(59,130,246,.15); + border: 2px dashed var(--primary, #4361ee); + border-radius: 6px; + margin: 2px 0; +} diff --git a/planka.php b/planka.php index 4af8c94..59ed373 100644 --- a/planka.php +++ b/planka.php @@ -150,24 +150,50 @@ function renderBoard() {
`; + // Drag source — cards area.querySelectorAll('.pk-card').forEach(el => { el.addEventListener('click', () => openCardModal(el.dataset.id)); el.addEventListener('dragstart', e => { - e.dataTransfer.setData('text/plain', el.dataset.id); + e.dataTransfer.setData('card-id', el.dataset.id); e.dataTransfer.effectAllowed = 'move'; - el.classList.add('is-dragging'); + setTimeout(() => el.classList.add('is-dragging'), 0); + }); + el.addEventListener('dragend', () => { + el.classList.remove('is-dragging'); + area.querySelectorAll('.drag-over,.drop-before-active').forEach(x => x.classList.remove('drag-over','drop-before-active')); }); - el.addEventListener('dragend', e => { el.classList.remove('is-dragging'); }); }); - // Drop target = toute la colonne .pk-cards + + // Drop zone — before a specific card + area.querySelectorAll('.pk-drop-before').forEach(el => { + el.addEventListener('dragover', e => { e.preventDefault(); e.stopPropagation(); el.classList.add('drop-before-active'); }); + el.addEventListener('dragleave', () => el.classList.remove('drop-before-active')); + el.addEventListener('drop', e => { + e.preventDefault(); e.stopPropagation(); + el.classList.remove('drop-before-active'); + const cardId = e.dataTransfer.getData('card-id'); + if (!cardId) return; + const listId = el.dataset.listId; + const prevPos = parseFloat(el.dataset.prevPos || 0); + const curPos = parseFloat(el.dataset.curPos || 0); + const newPos = prevPos === 0 ? curPos / 2 : (prevPos + curPos) / 2; + moveCard(cardId, listId, newPos); + }); + }); + + // Drop zone — end of column (whole .pk-cards) area.querySelectorAll('.pk-cards').forEach(el => { el.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; el.classList.add('drag-over'); }); el.addEventListener('dragleave', e => { if (!el.contains(e.relatedTarget)) el.classList.remove('drag-over'); }); el.addEventListener('drop', e => { - e.preventDefault(); + e.preventDefault(); e.stopPropagation(); el.classList.remove('drag-over'); - const cardId = e.dataTransfer.getData('text/plain'); - if (cardId) dropCard(el.dataset.listId, cardId); + const cardId = e.dataTransfer.getData('card-id'); + if (!cardId) return; + const listId = el.dataset.listId; + const listCards = state.cards.filter(c => c.listId === listId && c.id !== cardId); + const maxPos = listCards.reduce((m, c) => Math.max(m, c.position || 0), 0); + moveCard(cardId, listId, maxPos + 65535); }); }); } @@ -180,15 +206,15 @@ function renderList(list) { ${esc(list.name)} ${cards.length} -
+
${cards.length === 0 ? '
Déposer ici
' : ''} - ${cards.map(c => renderCard(c)).join('')} + ${cards.map((c, i) => renderCard(c, cards[i-1] ?? null)).join('')}
`; } -function renderCard(card) { +function renderCard(card, prevCard) { const labels = (state.cardLabels || []).filter(cl => cl.cardId === card.id).map(cl => { const lbl = (state.labels || []).find(l => l.id === cl.labelId); if (!lbl) return ''; @@ -203,24 +229,25 @@ function renderCard(card) { return `📅 ${formatDate(card.dueDate)}`; })() : ''; const taskHtml = cardTasks.length ? `☑ ${doneCount}/${cardTasks.length}` : ''; + const prevPos = prevCard ? (prevCard.position ?? 0) : 0; return ` -
+
+
${labels ? `
${labels}
` : ''}
${esc(card.name)}
${dueHtml || taskHtml ? `
${dueHtml}${taskHtml}
` : ''}
`; } -async function dropCard(listId, cardId) { +async function moveCard(cardId, listId, position) { const card = state.cards.find(c => c.id === cardId); - if (!card || card.listId === listId) return; - // Planka requires position when changing listId - const targetCards = state.cards.filter(c => c.listId === listId); - const maxPos = targetCards.reduce((m, c) => Math.max(m, c.position || 0), 0); - const position = maxPos + 65535; - const res = await apiFetch('update_card', { id: cardId }, 'PATCH', { listId, position }); + if (!card) return; + // Skip no-op (same list, same position range) + if (card.listId === listId && Math.abs((card.position || 0) - position) < 1) return; + const body = { listId, position }; + const res = await apiFetch('update_card', { id: cardId }, 'PATCH', body); if (!res.ok) { showToast('Erreur : ' + (res.error || 'déplacement impossible'), 'error'); return; } - card.listId = listId; + card.listId = listId; card.position = position; renderBoard(); showToast('Carte déplacée ✓'); @@ -229,7 +256,9 @@ async function dropCard(listId, cardId) { async function quickAddCard(listId) { const name = prompt('Nom de la carte :'); if (!name || !name.trim()) return; - const res = await apiFetch('create_card', { list_id: listId, board_id: state.activeBoardId, name: name.trim() }); + const listCards = state.cards.filter(c => c.listId === listId); + const maxPos = listCards.reduce((m, c) => Math.max(m, c.position || 0), 0); + const res = await apiFetch('create_card', { list_id: listId, name: name.trim(), position: maxPos + 65535 }); if (!res.ok) { showToast('Erreur: ' + res.error, 'error'); return; } state.cards.push(res.data); renderBoard();