fix(planka): endpoints v2, tri intra-liste, drag&drop robuste
Deploy HouseHub / deploy (push) Successful in 2s
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:
co-authored by
Claude Sonnet 4.6
parent
0db14560a5
commit
5828d1796d
+12
-9
@@ -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') {
|
||||||
|
|||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user