fix(planka): endpoints v2, tri intra-liste, drag&drop robuste
Deploy HouseHub / deploy (push) Successful in 2s

- 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 <noreply@anthropic.com>
This commit is contained in:
perco
2026-05-19 17:49:26 +02:00
co-authored by Claude Sonnet 4.6
parent 0db14560a5
commit 5828d1796d
3 changed files with 75 additions and 29 deletions
+12 -9
View File
@@ -99,25 +99,28 @@ if ($action === 'set_active_board') {
if ($action === 'create_list') { if ($action === 'create_list') {
$board_id = $_GET['board_id'] ?? null; if (!$board_id) tErr('board_id manquant'); $board_id = $_GET['board_id'] ?? null; if (!$board_id) tErr('board_id manquant');
$name = trim($_GET['name'] ?? ''); if (!$name) tErr('name 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', [ $resp = planka_api('POST', '/api/boards/' . $board_id . '/lists', [
'name' => $name, 'name' => $name,
'position' => $position, 'position' => $position,
'type' => 'active',
], $token); ], $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']); tOk($resp['item']);
} }
if ($action === 'create_card') { if ($action === 'create_card') {
$list_id = $_GET['list_id'] ?? null; if (!$list_id) tErr('list_id manquant'); $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');
$name = trim($_GET['name'] ?? ''); if (!$name) tErr('name manquant'); $pos = (float) ($_GET['position'] ?? 65535);
$resp = planka_api('POST', '/api/boards/' . $board_id . '/cards', [ // Planka v2+: POST /api/lists/{listId}/cards with type required
'listId' => $list_id, $resp = planka_api('POST', '/api/lists/' . $list_id . '/cards', [
'name' => $name, 'name' => $name,
'position' => 65535, 'position' => $pos,
'type' => 'story',
], $token); ], $token);
tOk($resp['item'] ?? []); if (empty($resp['item'])) tErr($resp['message'] ?? ($resp['problems'][0] ?? 'Erreur Planka'), 502);
tOk($resp['item']);
} }
if ($action === 'update_card') { if ($action === 'update_card') {
+14
View File
@@ -488,3 +488,17 @@
background: var(--bg-panel, #f3f4f6); background: var(--bg-panel, #f3f4f6);
color: var(--primary, #4361ee); 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;
}
+49 -20
View File
@@ -150,24 +150,50 @@ function renderBoard() {
<div class="pk-list pk-list-new"> <div class="pk-list pk-list-new">
<button class="pk-add-list-btn" onclick="promptCreateList()"> Ajouter une liste</button> <button class="pk-add-list-btn" onclick="promptCreateList()"> Ajouter une liste</button>
</div>`; </div>`;
// Drag source — cards
area.querySelectorAll('.pk-card').forEach(el => { area.querySelectorAll('.pk-card').forEach(el => {
el.addEventListener('click', () => openCardModal(el.dataset.id)); el.addEventListener('click', () => openCardModal(el.dataset.id));
el.addEventListener('dragstart', e => { el.addEventListener('dragstart', e => {
e.dataTransfer.setData('text/plain', el.dataset.id); e.dataTransfer.setData('card-id', el.dataset.id);
e.dataTransfer.effectAllowed = 'move'; 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 => { area.querySelectorAll('.pk-cards').forEach(el => {
el.addEventListener('dragover', e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; el.classList.add('drag-over'); }); 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('dragleave', e => { if (!el.contains(e.relatedTarget)) el.classList.remove('drag-over'); });
el.addEventListener('drop', e => { el.addEventListener('drop', e => {
e.preventDefault(); e.preventDefault(); e.stopPropagation();
el.classList.remove('drag-over'); el.classList.remove('drag-over');
const cardId = e.dataTransfer.getData('text/plain'); const cardId = e.dataTransfer.getData('card-id');
if (cardId) dropCard(el.dataset.listId, cardId); 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) {
<span class="pk-list-name">${esc(list.name)}</span> <span class="pk-list-name">${esc(list.name)}</span>
<span class="pk-list-count">${cards.length}</span> <span class="pk-list-count">${cards.length}</span>
</div> </div>
<div class="pk-cards" id="cards-${list.id}" data-list-id="${list.id}"> <div class="pk-cards" id="cards-${list.id}" data-list-id="${list.id}" data-drop-end="1">
${cards.length === 0 ? '<div class="pk-drop-hint">Déposer ici</div>' : ''} ${cards.length === 0 ? '<div class="pk-drop-hint">Déposer ici</div>' : ''}
${cards.map(c => renderCard(c)).join('')} ${cards.map((c, i) => renderCard(c, cards[i-1] ?? null)).join('')}
</div> </div>
<button class="pk-add-card-btn" onclick="quickAddCard('${list.id}')"> Ajouter une carte</button> <button class="pk-add-card-btn" onclick="quickAddCard('${list.id}')"> Ajouter une carte</button>
</div>`; </div>`;
} }
function renderCard(card) { function renderCard(card, prevCard) {
const labels = (state.cardLabels || []).filter(cl => cl.cardId === card.id).map(cl => { const labels = (state.cardLabels || []).filter(cl => cl.cardId === card.id).map(cl => {
const lbl = (state.labels || []).find(l => l.id === cl.labelId); const lbl = (state.labels || []).find(l => l.id === cl.labelId);
if (!lbl) return ''; if (!lbl) return '';
@@ -203,24 +229,25 @@ function renderCard(card) {
return `<span class="pk-due ${cls}">📅 ${formatDate(card.dueDate)}</span>`; return `<span class="pk-due ${cls}">📅 ${formatDate(card.dueDate)}</span>`;
})() : ''; })() : '';
const taskHtml = cardTasks.length ? `<span class="pk-task-count">☑ ${doneCount}/${cardTasks.length}</span>` : ''; const taskHtml = cardTasks.length ? `<span class="pk-task-count">☑ ${doneCount}/${cardTasks.length}</span>` : '';
const prevPos = prevCard ? (prevCard.position ?? 0) : 0;
return ` return `
<div class="pk-card" data-id="${card.id}" draggable="true"> <div class="pk-drop-before" data-list-id="${card.listId}" data-before-id="${card.id}" data-prev-pos="${prevPos}" data-cur-pos="${card.position ?? 0}"></div>
<div class="pk-card" data-id="${card.id}" data-list-id="${card.listId}" data-position="${card.position ?? 0}" draggable="true">
${labels ? `<div class="pk-card-labels">${labels}</div>` : ''} ${labels ? `<div class="pk-card-labels">${labels}</div>` : ''}
<div class="pk-card-name">${esc(card.name)}</div> <div class="pk-card-name">${esc(card.name)}</div>
${dueHtml || taskHtml ? `<div class="pk-card-meta">${dueHtml}${taskHtml}</div>` : ''} ${dueHtml || taskHtml ? `<div class="pk-card-meta">${dueHtml}${taskHtml}</div>` : ''}
</div>`; </div>`;
} }
async function dropCard(listId, cardId) { async function moveCard(cardId, listId, position) {
const card = state.cards.find(c => c.id === cardId); const card = state.cards.find(c => c.id === cardId);
if (!card || card.listId === listId) return; if (!card) return;
// Planka requires position when changing listId // Skip no-op (same list, same position range)
const targetCards = state.cards.filter(c => c.listId === listId); if (card.listId === listId && Math.abs((card.position || 0) - position) < 1) return;
const maxPos = targetCards.reduce((m, c) => Math.max(m, c.position || 0), 0); const body = { listId, position };
const position = maxPos + 65535; const res = await apiFetch('update_card', { id: cardId }, 'PATCH', body);
const res = await apiFetch('update_card', { id: cardId }, 'PATCH', { listId, position });
if (!res.ok) { showToast('Erreur : ' + (res.error || 'déplacement impossible'), 'error'); return; } if (!res.ok) { showToast('Erreur : ' + (res.error || 'déplacement impossible'), 'error'); return; }
card.listId = listId; card.listId = listId;
card.position = position; card.position = position;
renderBoard(); renderBoard();
showToast('Carte déplacée ✓'); showToast('Carte déplacée ✓');
@@ -229,7 +256,9 @@ async function dropCard(listId, cardId) {
async function quickAddCard(listId) { async function quickAddCard(listId) {
const name = prompt('Nom de la carte :'); const name = prompt('Nom de la carte :');
if (!name || !name.trim()) return; 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; } if (!res.ok) { showToast('Erreur: ' + res.error, 'error'); return; }
state.cards.push(res.data); state.cards.push(res.data);
renderBoard(); renderBoard();