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)
This commit is contained in:
@@ -195,6 +195,31 @@ async def set_col_mapping(
|
||||
return {"status": "ok", "column": column, "label": gitea_label}
|
||||
|
||||
|
||||
@router.delete("/col-mapping")
|
||||
async def delete_col_mapping(
|
||||
owner: str = Query(...),
|
||||
repo: str = Query(...),
|
||||
column: str = Query(...),
|
||||
):
|
||||
"""Delete a column-to-label mapping."""
|
||||
with get_conn() as conn:
|
||||
board = conn.execute(
|
||||
"SELECT id FROM boards WHERE project_owner=? AND project_name=?",
|
||||
(owner, repo),
|
||||
).fetchone()
|
||||
if not board:
|
||||
raise HTTPException(status_code=404, detail="Board not found")
|
||||
board_id = board["id"]
|
||||
|
||||
conn.execute(
|
||||
"DELETE FROM col_mapping WHERE board_id=? AND column_name=?",
|
||||
(board_id, column),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
return {"status": "ok", "column": column}
|
||||
|
||||
|
||||
@router.get("/board-config/{owner}/{repo}")
|
||||
async def get_board_config(owner: str, repo: str):
|
||||
with get_conn() as conn:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<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>
|
||||
@@ -82,6 +83,19 @@
|
||||
{% 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">×</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">
|
||||
@@ -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 = '<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,'"')}"]`);
|
||||
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 %}
|
||||
|
||||
@@ -3,7 +3,22 @@
|
||||
<div class="board-column" data-column="{{ col }}" id="col-{{ loop.index }}">
|
||||
<div class="column-header">
|
||||
<span class="column-title">{{ col }}</span>
|
||||
<span class="column-count">{{ column_map[col]|length }}</span>
|
||||
<div class="column-header-right">
|
||||
<span class="column-count">{{ column_map[col]|length }}</span>
|
||||
<button class="col-config-btn" onclick="toggleColConfig(event, '{{ owner }}','{{ repo }}','{{ col }}')" title="Configurer la colonne">⚙</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-config-panel" id="col-config-{{ col|replace(' ', '_') }}" style="display:none">
|
||||
<div class="col-config-inner">
|
||||
<label class="form-label">Label Gitea → cette colonne</label>
|
||||
<select class="form-select" id="col-label-{{ col|replace(' ', '_') }}">
|
||||
<option value="">— Aucun mapping —</option>
|
||||
</select>
|
||||
<div class="col-config-actions">
|
||||
<button class="btn btn-sm btn-primary" onclick="saveColMapping('{{ owner }}','{{ repo }}','{{ col }}')">Mapper</button>
|
||||
<button class="btn btn-sm" onclick="deleteColMapping('{{ owner }}','{{ repo }}','{{ col }}')">✕ Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column-cards sortable" data-column="{{ col }}" data-owner="{{ owner }}" data-repo="{{ repo }}">
|
||||
{% for issue in column_map[col] %}
|
||||
|
||||
Reference in New Issue
Block a user