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
+10 -7
View File
@@ -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,
$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') {
+14
View File
@@ -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;
}
+48 -19
View File
@@ -150,24 +150,50 @@ function renderBoard() {
<div class="pk-list pk-list-new">
<button class="pk-add-list-btn" onclick="promptCreateList()"> Ajouter une liste</button>
</div>`;
// 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', e => { el.classList.remove('is-dragging'); });
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'));
});
// 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) {
<span class="pk-list-name">${esc(list.name)}</span>
<span class="pk-list-count">${cards.length}</span>
</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.map(c => renderCard(c)).join('')}
${cards.map((c, i) => renderCard(c, cards[i-1] ?? null)).join('')}
</div>
<button class="pk-add-card-btn" onclick="quickAddCard('${list.id}')"> Ajouter une carte</button>
</div>`;
}
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,22 +229,23 @@ function renderCard(card) {
return `<span class="pk-due ${cls}">📅 ${formatDate(card.dueDate)}</span>`;
})() : '';
const taskHtml = cardTasks.length ? `<span class="pk-task-count">☑ ${doneCount}/${cardTasks.length}</span>` : '';
const prevPos = prevCard ? (prevCard.position ?? 0) : 0;
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>` : ''}
<div class="pk-card-name">${esc(card.name)}</div>
${dueHtml || taskHtml ? `<div class="pk-card-meta">${dueHtml}${taskHtml}</div>` : ''}
</div>`;
}
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.position = position;
@@ -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();