Files
flowdeck/app/templates/board.html
T
bruno c19503cf2f
CI / test (push) Failing after 4s
CI / lint (push) Failing after 4s
feat: UI de configuration des colonnes et mappings label→colonne
- Bouton ⚙ sur chaque colonne pour configurer le mapping label Gitea
- Modal ⚙ Colonnes pour ajouter/renommer/supprimer des colonnes
- API DELETE /api/col-mapping pour supprimer un mapping
- Rafraîchit le board après chaque modification de colonne/mapping
- CSS pour les nouveaux éléments (config panel, column list)
2026-07-08 11:33:08 -04:00

546 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block content %}
<div class="board"
x-data="{
owner: '{{ owner }}',
repo: '{{ repo }}',
milestone: '{{ current_milestone }}',
label: '{{ current_label }}',
assignee: '{{ current_assignee }}'
}">
<!-- Header -->
<div class="board-header">
<div class="board-breadcrumb">
<a href="/" class="back-link">← Projets</a>
<span class="separator">/</span>
<span class="repo-owner">{{ owner }}</span>
<span class="separator">/</span>
<strong>{{ repo }}</strong>
</div>
<div class="board-actions">
<button class="btn btn-sm" onclick="openBoardConfig('{{ owner }}', '{{ repo }}')" title="Gérer les colonnes">⚙ Colonnes</button>
<button class="btn btn-sm" onclick="registerWebhook('{{ owner }}', '{{ repo }}')" title="Enregistrer webhook">🔔 Webhook</button>
<a href="/notes/{{ owner }}/{{ repo }}" class="btn btn-sm">📝 Notes</a>
<button class="btn btn-sm btn-primary" onclick="openCreateModal('{{ owner }}','{{ repo }}','Backlog')">+ Issue</button>
</div>
</div>
<!-- Filters -->
<div class="board-filters">
<select class="filter-select" name="milestone_filter"
hx-get="/board/{{ owner }}/{{ repo }}?fragment=1"
hx-target="#board-columns"
hx-trigger="change"
hx-include="[name='label_filter'],[name='assignee_filter']"
hx-swap="outerHTML">
<option value="">Tous les milestones</option>
{% for m in milestones %}
<option value="{{ m.title }}" {% if m.title == current_milestone %}selected{% endif %}>{{ m.title }}</option>
{% endfor %}
</select>
<select class="filter-select" name="label_filter"
hx-get="/board/{{ owner }}/{{ repo }}?fragment=1"
hx-target="#board-columns"
hx-trigger="change"
hx-include="[name='milestone_filter'],[name='assignee_filter']"
hx-swap="outerHTML">
<option value="">Tous les labels</option>
{% for lbl in labels %}
<option value="{{ lbl.name }}" {% if lbl.name == current_label %}selected{% endif %}>🏷 {{ lbl.name }}</option>
{% endfor %}
</select>
<input type="text" name="assignee_filter" placeholder="Assigné à..."
class="filter-input"
hx-get="/board/{{ owner }}/{{ repo }}?fragment=1"
hx-target="#board-columns"
hx-trigger="keyup changed delay:500ms"
hx-include="[name='milestone_filter'],[name='label_filter']"
hx-swap="outerHTML"
value="{{ current_assignee }}">
<button class="btn btn-sm" onclick="window.location.reload()">🔄 Rafraîchir</button>
</div>
<!-- Columns (fragment) -->
{% include "board_fragment.html" %}
<!-- PRs section -->
{% if prs %}
<div class="prs-section">
<h3>🔄 Pull Requests ({{ prs|length }})</h3>
<div class="prs-list">
{% for pr in prs %}
<div class="pr-card">
<a href="{{ pr.html_url }}" target="_blank" class="pr-title">#{{ pr.number }} {{ pr.title[:80] }}</a>
<div class="pr-meta">
{% if pr.user %}<span>{{ pr.user.login }}</span>{% endif %}
<span>{{ pr.state }}</span>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Board Config Modal (column management) -->
<div id="board-config-modal" class="modal-overlay" style="display:none">
<div class="modal-container">
<div class="modal-header">
<h3>⚙ Configuration des colonnes</h3>
<button onclick="closeBoardConfig()" class="modal-close">&times;</button>
</div>
<div class="modal-body" id="board-config-content">
<div class="loading">Chargement...</div>
</div>
</div>
</div>
<!-- Create Issue Modal (reuses card-modal) -->
<div id="create-modal" class="modal-overlay" style="display:none">
<div class="modal-container">
<div class="modal-header">
<h3 id="create-modal-title">Nouvelle issue</h3>
<button onclick="closeCreateModal()" class="modal-close">&times;</button>
</div>
<div id="create-modal-content" class="modal-body"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// ── Labels data (passed from server) ──
const ALL_LABELS = {{ labels | tojson }};
const ALL_MILESTONES = {{ milestones | tojson }};
function getCSRFToken() {
const m = document.cookie.match(/csrf_token=([^;]+)/);
return m ? m[1] : '';
}
// ── Create Modal ──
function openCreateModal(owner, repo, defaultColumn) {
document.getElementById('card-modal').style.display = 'none';
const modal = document.getElementById('create-modal');
const content = document.getElementById('create-modal-content');
document.getElementById('create-modal-title').textContent = `Nouvelle issue — ${owner}/${repo}`;
let h = '<form onsubmit="saveNewIssue(event,\''+owner+'\',\''+repo+'\',\''+defaultColumn+'\')">';
h += '<label class="form-label">Titre *</label>';
h += '<input type="text" id="create-title" class="form-input" placeholder="Titre de l\'issue..." required>';
h += '<label class="form-label">Description</label>';
h += '<textarea id="create-body" rows="4" class="form-input" placeholder="Description (markdown)..."></textarea>';
// Multi-label checkboxes
h += '<label class="form-label">Labels</label>';
h += '<div class="label-checkboxes">';
if (ALL_LABELS.length === 0) {
h += '<span style="color:var(--text-dim);font-size:12px">Aucun label dans ce repo. <a href="'+window._giteaUrl+'/'+owner+'/'+repo+'/labels" target="_blank">En créer dans Gitea</a></span>';
} else {
ALL_LABELS.forEach(l => {
h += '<label class="label-cb"><input type="checkbox" name="labels" value="'+l.id+'"> <span class="card-label" style="background:#'+(l.color||'666')+'33;color:#'+(l.color||'ccc')+'">'+escHtml(l.name)+'</span></label>';
});
}
h += '</div>';
// Milestone
h += '<label class="form-label">Milestone</label>';
h += '<select id="create-milestone" class="form-select">';
h += '<option value="">Aucun</option>';
ALL_MILESTONES.forEach(m => { h += '<option value="'+m.id+'">'+escHtml(m.title)+'</option>'; });
h += '</select>';
// Assignee
h += '<label class="form-label">Assigné à</label>';
h += '<input type="text" id="create-assignee" class="form-input" placeholder="Login Gitea...">';
h += '<div class="detail-actions" style="margin-top:12px">';
h += '<button type="submit" class="btn btn-primary">💾 Créer</button>';
h += '<button type="button" class="btn" onclick="closeCreateModal()">Annuler</button>';
h += '</div></form>';
content.innerHTML = h;
modal.style.display = 'flex';
}
function closeCreateModal() {
document.getElementById('create-modal').style.display = 'none';
}
async function saveNewIssue(evt, owner, repo, defaultColumn) {
evt.preventDefault();
const title = document.getElementById('create-title').value.trim();
const body = document.getElementById('create-body').value.trim();
const milestone = document.getElementById('create-milestone').value;
const assignee = document.getElementById('create-assignee').value.trim();
if (!title) return;
// Gather checked labels
const checkedLabels = [];
document.querySelectorAll('#create-modal-content input[name="labels"]:checked').forEach(cb => {
checkedLabels.push(cb.value);
});
const params = new URLSearchParams({title, body, milestone, assignee});
if (checkedLabels.length) params.set('labels', checkedLabels.join(','));
const resp = await fetch(`/api/issues/${owner}/${repo}?${params}`, {
method: 'POST', headers: {'X-CSRF-Token': getCSRFToken()}
});
if (resp.ok) {
closeCreateModal();
window.location.reload();
} else {
alert('Erreur lors de la création');
}
}
// ── Column double-click → create with column preselected ──
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.board-column').forEach(col => {
col.addEventListener('dblclick', function(evt) {
// Only if NOT clicking on a card
if (evt.target.closest('.card')) return;
const owner = this.querySelector('.column-cards')?.dataset.owner;
const repo = this.querySelector('.column-cards')?.dataset.repo;
const column = this.dataset.column || 'Backlog';
if (owner && repo) openCreateModal(owner, repo, column);
});
});
});
document.addEventListener('htmx:afterSettle', () => {
document.querySelectorAll('.board-column').forEach(col => {
if (col._hasDblClick) return;
col._hasDblClick = true;
col.addEventListener('dblclick', function(evt) {
if (evt.target.closest('.card')) return;
const owner = this.querySelector('.column-cards')?.dataset.owner;
const repo = this.querySelector('.column-cards')?.dataset.repo;
const column = this.dataset.column || 'Backlog';
if (owner && repo) openCreateModal(owner, repo, column);
});
});
});
// ── SortableJS ──
function initSortable() {
document.querySelectorAll('.sortable').forEach(el => {
if (el._sortable) el._sortable.destroy();
el._sortable = new Sortable(el, {
group: 'kanban', animation: 150, ghostClass: 'card-ghost',
onEnd: async function(evt) {
const card = evt.item, issueId = card.dataset.issueId;
const newCol = evt.to.dataset.column, owner = evt.to.dataset.owner, repo = evt.to.dataset.repo;
try {
const resp = await fetch(`/api/move?owner=${owner}&repo=${repo}&issue_id=${issueId}&column=${encodeURIComponent(newCol)}`, {
method: 'POST', headers: {'X-CSRF-Token': getCSRFToken()}
});
if (!resp.ok) throw new Error('Move failed');
document.querySelectorAll('.board-column').forEach(col => {
const c = col.querySelectorAll('.card').length;
const b = col.querySelector('.column-count');
if (b) b.textContent = c;
});
} catch(e) { console.error('Move failed:', e); window.location.reload(); }
}
});
});
}
document.addEventListener('DOMContentLoaded', initSortable);
document.addEventListener('htmx:afterSettle', initSortable);
// ── Card Detail ──
function openCardDetail(owner, repo, issueId) {
document.getElementById('create-modal').style.display = 'none';
const modal = document.getElementById('card-modal');
const content = document.getElementById('card-modal-content');
content.innerHTML = '<div class="loading">Chargement...</div>';
document.getElementById('card-modal-title').textContent = `#${issueId} — ${owner}/${repo}`;
modal.style.display = 'flex';
fetch(`/api/issues/${owner}/${repo}/${issueId}`)
.then(r => r.json()).then(data => { content.innerHTML = renderCardDetail(owner, repo, data); })
.catch(e => { content.innerHTML = '<p class="error">Erreur: '+e+'</p>'; });
}
function closeModal() { document.getElementById('card-modal').style.display = 'none'; }
function renderCardDetail(owner, repo, data) {
const issue = data.issue || {}, comments = data.comments || [], checklists = data.checklists || [], card = data.card || {};
window._issueData = {owner, repo, issue, comments, checklists, card};
const labelsHtml = (issue.labels || []).map(l =>
`<span class="card-label" style="background:#${l.color||'666'}33;color:#${l.color||'ccc'}">${escHtml(l.name)}</span>`
).join('');
const prio = card.priority || 'Medium';
const prioOpts = ['Low','Medium','High','Urgent'].map(p =>
`<option value="${p}" ${prio===p?'selected':''}>${p}</option>`
).join('');
return `
<div class="detail-section"><span class="detail-state badge ${issue.state}">${issue.state}</span> <strong>${escHtml(issue.title||'')}</strong></div>
<div class="detail-meta">
${labelsHtml}
${issue.milestone ? '<span>📅 '+escHtml(issue.milestone.title)+'</span>' : ''}
${issue.assignee ? '<span>👤 '+escHtml(issue.assignee.login)+'</span>' : ''}
</div>
<div class="detail-editable">
<label>Priorité: <select onchange="updateCardMeta('${owner}','${repo}',${issue.number},this.value,'')" class="form-select-sm">${prioOpts}</select></label>
<label>Échéance: <input type="date" value="${card.due_date||''}" onchange="updateCardMeta('${owner}','${repo}',${issue.number},'',this.value)" class="form-input-sm"></label>
</div>
<div class="detail-section"><h4>Description</h4><div class="markdown-body">${escHtml(issue.body||'')}</div></div>
${renderChecklists(owner, repo, issue.number, checklists)}
<div class="detail-section"><h4>Commentaires (${comments.length})</h4>
${comments.map(c => `
<div class="comment">
<img src="${c.user?.avatar_url||''}" width="24" height="24" class="avatar">
<strong>${escHtml(c.user?.login||'')}</strong>
<span class="comment-date">${(c.created_at||'').substr(0,10)}</span>
<div class="comment-body">${escHtml(c.body||'')}</div>
</div>`).join('')}
</div>
<div class="detail-actions">
<a href="${issue.html_url||''}" target="_blank" class="btn btn-sm">🔗 Ouvrir dans Gitea</a>
<button onclick="showEditForm('${owner}','${repo}',${issue.number})" class="btn btn-sm">✏️ Éditer</button>
<button onclick="closeModal()" class="btn btn-sm">Fermer</button>
</div>`;
}
function renderChecklists(owner, repo, issueId, checklists) {
let h = '<div class="detail-section"><h4>Checklists</h4>';
checklists.forEach(cl => {
h += '<div class="checklist"><strong>'+escHtml(cl.title)+'</strong><ul class="checklist-items">';
(cl.items||[]).forEach(item => {
h += '<li><input type="checkbox" '+(item.checked?'checked':'')+' onchange="toggleCheckItem('+item.id+',this.checked)" class="checklist-check">';
h += '<span class="'+(item.checked?'checked':'')+'">'+escHtml(item.content)+'</span>';
h += '<button onclick="deleteCheckItem('+item.id+')" class="btn-del">×</button></li>';
});
h += '</ul><div class="add-item"><input type="text" id="new-item-'+cl.id+'" placeholder="Nouvelle tâche..." class="form-input-sm" onkeydown="if(event.key===\'Enter\')addCheckItem('+cl.id+',\''+owner+'\',\''+repo+'\','+issueId+')">';
h += '<button onclick="addCheckItem('+cl.id+',\''+owner+'\',\''+repo+'\','+issueId+')" class="btn btn-sm">+</button></div></div>';
});
if (!checklists.length) h += '<button onclick="createChecklist(\''+owner+'\',\''+repo+'\','+issueId+')" class="btn btn-sm">+ Nouvelle checklist</button>';
h += '</div>';
return h;
}
// ── Edit Form ──
function showEditForm(owner, repo, issueId) {
const content = document.getElementById('card-modal-content');
const data = window._issueData || {}, issue = data.issue || {};
let h = '<form onsubmit="saveIssueEdit(event,\''+owner+'\',\''+repo+'\','+issueId+')">';
h += '<label class="form-label">Titre</label>';
h += '<input type="text" id="edit-title" value="'+escHtml(issue.title||'')+'" class="form-input" required>';
h += '<label class="form-label">Description</label>';
h += '<textarea id="edit-body" rows="5" class="form-input">'+escHtml(issue.body||'')+'</textarea>';
h += '<label class="form-label">Labels</label>';
h += '<div class="label-checkboxes">';
const issueLabelIds = (issue.labels||[]).map(l => l.id);
if (!ALL_LABELS.length) {
h += '<span style="color:var(--text-dim);font-size:12px">Aucun label</span>';
} else {
ALL_LABELS.forEach(l => {
h += '<label class="label-cb"><input type="checkbox" name="edit-labels" value="'+l.id+'" '+(issueLabelIds.includes(l.id)?'checked':'')+'> <span class="card-label" style="background:#'+(l.color||'666')+'33;color:#'+(l.color||'ccc')+'">'+escHtml(l.name)+'</span></label>';
});
}
h += '</div>';
h += '<label class="form-label">Assigné à</label>';
h += '<input type="text" id="edit-assignee" value="'+(issue.assignee?issue.assignee.login:'')+'" placeholder="Login Gitea..." class="form-input">';
h += '<div class="detail-actions" style="margin-top:8px">';
h += '<button type="submit" class="btn btn-primary">💾 Sauvegarder</button>';
h += '<button type="button" onclick="openCardDetail(\''+owner+'\',\''+repo+'\','+issueId+')" class="btn btn-sm">Annuler</button>';
h += '</div></form>';
content.innerHTML = h;
}
async function saveIssueEdit(evt, owner, repo, issueId) {
evt.preventDefault();
const title = document.getElementById('edit-title').value.trim();
const body = document.getElementById('edit-body').value.trim();
const assignee = document.getElementById('edit-assignee').value.trim();
if (!title) return;
const checkedLabels = [];
document.querySelectorAll('#card-modal-content input[name="edit-labels"]:checked').forEach(cb => checkedLabels.push(cb.value));
const params = new URLSearchParams({title, body, assignee});
if (checkedLabels.length) params.set('labels', checkedLabels.join(','));
const resp = await fetch(`/api/issues/${owner}/${repo}/${issueId}?${params}`, {
method: 'PATCH', headers: {'X-CSRF-Token': getCSRFToken()}
});
if (resp.ok) {
closeModal();
// Refresh board columns to reposition card
htmx.ajax('GET', `/board/${owner}/${repo}?fragment=1`, {target: '#board-columns', swap: 'outerHTML'});
} else {
alert('Erreur lors de la mise à jour');
}
}
// ── Helpers ──
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
async function updateCardMeta(o,r,i,p,d) { await fetch('/api/card/'+o+'/'+r+'/'+i+'?priority='+p+'&due_date='+d,{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}}); }
async function toggleCheckItem(id,checked) { await fetch('/api/checklist-items/'+id+'?checked='+checked,{method:'PATCH',headers:{'X-CSRF-Token':getCSRFToken()}}); }
async function deleteCheckItem(id) {
await fetch('/api/checklist-items/'+id,{method:'DELETE',headers:{'X-CSRF-Token':getCSRFToken()}});
const t = document.getElementById('card-modal-title').textContent;
const parts = t.split(' — '), n = parts[0].replace('#','');
const or = parts[1]?parts[1].split('/'):['',''];
if(or[0]&&or[1]) openCardDetail(or[0],or[1],parseInt(n));
}
async function addCheckItem(cid,o,r,iid) {
const inp = document.getElementById('new-item-'+cid);
if (!inp||!inp.value.trim()) return;
await fetch('/api/checklist-items/'+o+'/'+r+'/'+iid+'/'+cid+'?content='+encodeURIComponent(inp.value),{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}});
openCardDetail(o,r,iid);
}
async function createChecklist(o,r,iid) { await fetch('/api/checklists/'+o+'/'+r+'/'+iid,{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}}); openCardDetail(o,r,iid); }
async function registerWebhook(o,r) {
try {
const resp = await fetch('/api/webhook/register/'+o+'/'+r,{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}});
const d = await resp.json();
alert(d.status==='already_registered'?'Webhook déjà enregistré':'Webhook enregistré');
} catch(e) { alert('Erreur: '+e); }
}
// ── Column Configuration ──
let _colMappings = {};
function toggleColConfig(evt, owner, repo, col) {
evt.stopPropagation();
const safeId = col.replace(/ /g, '_');
const panel = document.getElementById('col-config-' + safeId);
if (!panel) return;
// Close all others
document.querySelectorAll('.col-config-panel').forEach(p => { if (p !== panel) p.style.display = 'none'; });
const isOpen = panel.style.display === 'block';
panel.style.display = isOpen ? 'none' : 'block';
if (!isOpen) {
// Populate label select
const sel = document.getElementById('col-label-' + safeId);
if (sel && sel.options.length <= 1) {
sel.innerHTML = '<option value="">— Aucun mapping —</option>';
ALL_LABELS.forEach(l => {
sel.innerHTML += '<option value="'+escHtml(l.name)+'">'+escHtml(l.name)+'</option>';
});
}
// Pre-select current mapping
if (_colMappings[col]) sel.value = _colMappings[col];
}
}
async function saveColMapping(owner, repo, col) {
const safeId = col.replace(/ /g, '_');
const sel = document.getElementById('col-label-' + safeId);
if (!sel) return;
const label = sel.value;
if (!label) { alert('Sélectionnez un label'); return; }
const params = new URLSearchParams({owner, repo, column: col, gitea_label: label});
await fetch('/api/col-mapping?' + params, {method:'POST', headers:{'X-CSRF-Token': getCSRFToken()}});
_colMappings[col] = label;
document.getElementById('col-config-' + safeId).style.display = 'none';
htmx.ajax('GET', `/board/${owner}/${repo}?fragment=1`, {target: '#board-columns', swap: 'outerHTML'});
}
async function deleteColMapping(owner, repo, col) {
if (!confirm('Supprimer le mapping pour « ' + col + ' » ?')) return;
const safeId = col.replace(/ /g, '_');
await fetch(`/api/col-mapping?owner=${owner}&repo=${repo}&column=${encodeURIComponent(col)}`, {method:'DELETE', headers:{'X-CSRF-Token': getCSRFToken()}});
delete _colMappings[col];
document.getElementById('col-config-' + safeId).style.display = 'none';
htmx.ajax('GET', `/board/${owner}/${repo}?fragment=1`, {target: '#board-columns', swap: 'outerHTML'});
}
// ── Board Config (Column Management) ──
async function openBoardConfig(owner, repo) {
document.getElementById('card-modal').style.display = 'none';
document.getElementById('create-modal').style.display = 'none';
const modal = document.getElementById('board-config-modal');
const content = document.getElementById('board-config-content');
modal.style.display = 'flex';
content.innerHTML = '<div class="loading">Chargement...</div>';
const resp = await fetch(`/api/board-config/${owner}/${repo}`);
const cfg = await resp.json();
let h = '<div class="detail-section"><h4>Colonnes actuelles</h4>';
h += '<ul class="config-column-list" id="config-column-list">';
cfg.columns.forEach((c, i) => {
h += '<li class="config-column-row" data-col="'+escHtml(c)+'">';
h += '<input type="text" value="'+escHtml(c)+'" class="form-input config-col-name" style="width:200px;margin:0" data-orig="'+escHtml(c)+'">';
const mapped = (cfg.col_mappings||[]).find(m => m.column_name === c);
const labelName = mapped ? mapped.gitea_label : '';
h += '<span class="col-mapping-badge" style="font-size:11px;color:var(--text-dim);margin:0 8px">' + (labelName ? '🏷 '+escHtml(labelName) : '') + '</span>';
h += '<button class="btn btn-sm" onclick="renameColumn(\''+owner+'\',\''+repo+'\','+i+',\''+escHtml(c)+'\')">Renommer</button>';
h += '<button class="btn btn-sm btn-del" onclick="deleteColumn(\''+owner+'\',\''+repo+'\','+i+',\''+escHtml(c)+'\')">✕</button>';
h += '</li>';
});
h += '</ul></div>';
h += '<div class="detail-section"><h4>Ajouter une colonne</h4>';
h += '<div class="form-row">';
h += '<input type="text" id="new-col-name" class="form-input" placeholder="Nom de la colonne..." style="flex:1">';
h += '<button class="btn btn-primary btn-sm" onclick="addColumn(\''+owner+'\',\''+repo+'\')">+ Ajouter</button>';
h += '</div></div>';
h += '<div class="detail-actions">';
h += '<button onclick="closeBoardConfig()" class="btn btn-sm">Fermer</button>';
h += '</div>';
content.innerHTML = h;
window._boardConfig = cfg;
}
function closeBoardConfig() {
document.getElementById('board-config-modal').style.display = 'none';
}
async function addColumn(owner, repo) {
const inp = document.getElementById('new-col-name');
const name = inp.value.trim();
if (!name) return;
const cfg = window._boardConfig;
const cols = [...cfg.columns, name];
await fetch(`/api/board-config/${owner}/${repo}?columns=${encodeURIComponent(JSON.stringify(cols))}`, {method:'POST', headers:{'X-CSRF-Token': getCSRFToken()}});
closeBoardConfig();
htmx.ajax('GET', `/board/${owner}/${repo}?fragment=1`, {target: '#board-columns', swap: 'outerHTML'});
}
async function renameColumn(owner, repo, idx, oldName) {
const row = document.querySelector(`.config-column-row[data-col="${oldName.replace(/"/g,'&quot;')}"]`);
const inp = row ? row.querySelector('.config-col-name') : null;
const newName = inp ? inp.value.trim() : '';
if (!newName || newName === oldName) return;
const cfg = window._boardConfig;
cfg.columns[idx] = newName;
await fetch(`/api/board-config/${owner}/${repo}?columns=${encodeURIComponent(JSON.stringify(cfg.columns))}`, {method:'POST', headers:{'X-CSRF-Token': getCSRFToken()}});
// Update col_mapping if one existed for old name
const oldMapping = (cfg.col_mappings||[]).find(m => m.column_name === oldName);
if (oldMapping) {
await fetch(`/api/col-mapping?owner=${owner}&repo=${repo}&column=${encodeURIComponent(newName)}&gitea_label=${oldMapping.gitea_label}`, {method:'POST', headers:{'X-CSRF-Token': getCSRFToken()}});
}
closeBoardConfig();
htmx.ajax('GET', `/board/${owner}/${repo}?fragment=1`, {target: '#board-columns', swap: 'outerHTML'});
}
async function deleteColumn(owner, repo, idx, colName) {
if (!confirm('Supprimer la colonne « ' + colName + ' » ? Les cartes iront dans la première colonne.')) return;
const cfg = window._boardConfig;
const cols = cfg.columns.filter((_, i) => i !== idx);
await fetch(`/api/board-config/${owner}/${repo}?columns=${encodeURIComponent(JSON.stringify(cols))}`, {method:'POST', headers:{'X-CSRF-Token': getCSRFToken()}});
// Delete col_mapping
await fetch(`/api/col-mapping?owner=${owner}&repo=${repo}&column=${encodeURIComponent(colName)}`, {method:'DELETE', headers:{'X-CSRF-Token': getCSRFToken()}});
closeBoardConfig();
htmx.ajax('GET', `/board/${owner}/${repo}?fragment=1`, {target: '#board-columns', swap: 'outerHTML'});
}
// Load col mappings on init
(async function() {
const owner = '{{ owner }}';
const repo = '{{ repo }}';
try {
const resp = await fetch(`/api/board-config/${owner}/${repo}`);
const cfg = await resp.json();
(cfg.col_mappings||[]).forEach(m => { _colMappings[m.column_name] = m.gitea_label; });
} catch(e) {}
})();
</script>
{% endblock %}