@@ -385,5 +399,147 @@
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 = '
';
+ ALL_LABELS.forEach(l => {
+ sel.innerHTML += '
';
+ });
+ }
+ // 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 = '
Chargement...
';
+
+ const resp = await fetch(`/api/board-config/${owner}/${repo}`);
+ const cfg = await resp.json();
+
+ let h = '
Colonnes actuelles
';
+ h += '
';
+
+ h += '
';
+
+ h += '
';
+ h += '';
+ h += '
';
+
+ 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,'"')}"]`);
+ 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) {}
+ })();
{% endblock %}
diff --git a/app/templates/board_fragment.html b/app/templates/board_fragment.html
index d25f755..aeb8df3 100644
--- a/app/templates/board_fragment.html
+++ b/app/templates/board_fragment.html
@@ -3,7 +3,22 @@
{{ col }}
-
{{ column_map[col]|length }}
+
+ {{ column_map[col]|length }}
+
+
+
+
+
+
+
+
+
+
+
+
{% for issue in column_map[col] %}
diff --git a/static/css/app.css b/static/css/app.css
index a59fb7d..5dc18a2 100644
--- a/static/css/app.css
+++ b/static/css/app.css
@@ -119,6 +119,25 @@ body { min-height: 100vh; }
padding: 10px 12px; display: flex; justify-content: space-between; align-items: center;
border-bottom: 1px solid var(--border); flex-shrink: 0;
}
+.column-header-right { display: flex; align-items: center; gap: 6px; }
+.col-config-btn {
+ background: none; border: none; cursor: pointer; font-size: 13px;
+ color: var(--text-dim); padding: 2px 4px; border-radius: 4px;
+ line-height: 1; opacity: 0.5; transition: opacity 0.15s;
+}
+.col-config-btn:hover { opacity: 1; color: var(--accent); }
+.col-config-panel {
+ padding: 8px 12px; border-bottom: 1px solid var(--border);
+ background: var(--bg);
+}
+.col-config-inner { display: flex; flex-direction: column; gap: 6px; }
+.col-config-actions { display: flex; gap: 6px; }
+.config-column-list {
+ list-style: none; display: flex; flex-direction: column; gap: 8px;
+}
+.config-column-row {
+ display: flex; align-items: center; gap: 8px;
+}
.column-title { font-size: 13px; font-weight: 600; }
.column-count {
background: var(--border); border-radius: 10px; padding: 1px 8px;