fix(planka): drag&drop via dataTransfer + create_list + synchro position
Deploy HouseHub / deploy (push) Successful in 2s
Deploy HouseHub / deploy (push) Successful in 2s
- Drag & drop: utilise e.dataTransfer (pas de variable globale fragile)
- dropCard prend cardId en paramètre, envoie position requis par Planka
- Nouveau bouton 'Ajouter une liste' en fin de board
- Endpoint create_list dans api.php (POST /api/boards/{id}/lists)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
7d62aa02be
commit
0db14560a5
@@ -96,6 +96,18 @@ if ($action === 'set_active_board') {
|
|||||||
tOk(['updated' => true]);
|
tOk(['updated' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
$resp = planka_api('POST', '/api/boards/' . $board_id . '/lists', [
|
||||||
|
'name' => $name,
|
||||||
|
'position' => $position,
|
||||||
|
], $token);
|
||||||
|
if (empty($resp['item'])) tErr($resp['message'] ?? 'Planka n\'a pas créé la liste', 502);
|
||||||
|
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');
|
$board_id = $_GET['board_id'] ?? null; if (!$board_id) tErr('board_id manquant');
|
||||||
|
|||||||
@@ -463,3 +463,28 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin-bottom: .35rem;
|
margin-bottom: .35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pk-list-new {
|
||||||
|
background: transparent;
|
||||||
|
border: 2px dashed var(--border-light, #e5e7eb);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding: .75rem .5rem;
|
||||||
|
}
|
||||||
|
.pk-add-list-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted, #9ca3af);
|
||||||
|
font-size: .9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: .4rem .75rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background .15s, color .15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.pk-add-list-btn:hover {
|
||||||
|
background: var(--bg-panel, #f3f4f6);
|
||||||
|
color: var(--primary, #4361ee);
|
||||||
|
}
|
||||||
|
|||||||
+34
-14
@@ -94,7 +94,6 @@ require __DIR__ . '/header.php';
|
|||||||
const API = 'modules/planka/api.php';
|
const API = 'modules/planka/api.php';
|
||||||
let state = { boards: [], activeBoardId: null, lists: [], cards: [], labels: [], cardLabels: [], tasks: [] };
|
let state = { boards: [], activeBoardId: null, lists: [], cards: [], labels: [], cardLabels: [], tasks: [] };
|
||||||
let editingCard = null;
|
let editingCard = null;
|
||||||
let dragCardId = null;
|
|
||||||
|
|
||||||
async function apiFetch(action, params = {}, method = 'GET', body = null) {
|
async function apiFetch(action, params = {}, method = 'GET', body = null) {
|
||||||
const qs = new URLSearchParams({ action, ...params }).toString();
|
const qs = new URLSearchParams({ action, ...params }).toString();
|
||||||
@@ -146,19 +145,30 @@ async function loadBoard(id) {
|
|||||||
|
|
||||||
function renderBoard() {
|
function renderBoard() {
|
||||||
const area = document.getElementById('pk-board-area');
|
const area = document.getElementById('pk-board-area');
|
||||||
if (!state.lists.length) { area.innerHTML = '<div class="pk-loading">Aucune liste dans ce board.</div>'; return; }
|
|
||||||
const sorted = [...state.lists].sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
const sorted = [...state.lists].sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
||||||
area.innerHTML = sorted.map(list => renderList(list)).join('');
|
area.innerHTML = sorted.map(list => renderList(list)).join('') + `
|
||||||
|
<div class="pk-list pk-list-new">
|
||||||
|
<button class="pk-add-list-btn" onclick="promptCreateList()">+ Ajouter une liste</button>
|
||||||
|
</div>`;
|
||||||
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 => { dragCardId = el.dataset.id; el.classList.add('is-dragging'); e.stopPropagation(); });
|
el.addEventListener('dragstart', e => {
|
||||||
el.addEventListener('dragend', e => { el.classList.remove('is-dragging'); dragCardId = null; });
|
e.dataTransfer.setData('text/plain', el.dataset.id);
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
el.classList.add('is-dragging');
|
||||||
|
});
|
||||||
|
el.addEventListener('dragend', e => { el.classList.remove('is-dragging'); });
|
||||||
});
|
});
|
||||||
// Drop target = toute la colonne .pk-cards (pas juste une zone invisible de 4px)
|
// Drop target = toute la colonne .pk-cards
|
||||||
area.querySelectorAll('.pk-cards').forEach(el => {
|
area.querySelectorAll('.pk-cards').forEach(el => {
|
||||||
el.addEventListener('dragover', e => { e.preventDefault(); 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 => { e.preventDefault(); el.classList.remove('drag-over'); dropCard(el.dataset.listId); });
|
el.addEventListener('drop', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
el.classList.remove('drag-over');
|
||||||
|
const cardId = e.dataTransfer.getData('text/plain');
|
||||||
|
if (cardId) dropCard(el.dataset.listId, cardId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,20 +211,19 @@ function renderCard(card) {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function dropCard(listId) {
|
async function dropCard(listId, cardId) {
|
||||||
if (!dragCardId) return;
|
const card = state.cards.find(c => c.id === cardId);
|
||||||
const card = state.cards.find(c => c.id === dragCardId);
|
|
||||||
if (!card || card.listId === listId) return;
|
if (!card || card.listId === listId) return;
|
||||||
// Planka requires position when changing listId
|
// Planka requires position when changing listId
|
||||||
const targetCards = state.cards.filter(c => c.listId === listId);
|
const targetCards = state.cards.filter(c => c.listId === listId);
|
||||||
const maxPos = targetCards.reduce((m, c) => Math.max(m, c.position || 0), 0);
|
const maxPos = targetCards.reduce((m, c) => Math.max(m, c.position || 0), 0);
|
||||||
const position = maxPos + 65535;
|
const position = maxPos + 65535;
|
||||||
const res = await apiFetch('update_card', { id: dragCardId }, 'PATCH', { listId, position });
|
const res = await apiFetch('update_card', { id: cardId }, 'PATCH', { listId, position });
|
||||||
if (!res.ok) { showToast('Erreur déplacement : ' + (res.error || 'inconnu'), '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 ✓');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function quickAddCard(listId) {
|
async function quickAddCard(listId) {
|
||||||
@@ -327,6 +336,17 @@ async function saveSettings() {
|
|||||||
setTimeout(() => init(), 800);
|
setTimeout(() => init(), 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function promptCreateList() {
|
||||||
|
const name = prompt('Nom de la nouvelle liste :');
|
||||||
|
if (!name || !name.trim()) return;
|
||||||
|
const maxPos = state.lists.reduce((m, l) => Math.max(m, l.position || 0), 0);
|
||||||
|
const res = await apiFetch('create_list', { board_id: state.activeBoardId, name: name.trim(), position: maxPos + 65535 });
|
||||||
|
if (!res.ok) { showToast('Erreur : ' + (res.error || 'création impossible'), 'error'); return; }
|
||||||
|
state.lists.push(res.data);
|
||||||
|
renderBoard();
|
||||||
|
showToast('Liste créée ✓');
|
||||||
|
}
|
||||||
|
|
||||||
function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
|
function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
|
||||||
|
|
||||||
function formatDate(iso) {
|
function formatDate(iso) {
|
||||||
|
|||||||
Reference in New Issue
Block a user