feat: multi-label selection + double-click column to create issue
CI / test (push) Failing after 4s
CI / lint (push) Failing after 4s

- Labels now rendered as checkboxes (not single-select dropdown)
  in both create modal and edit form
- ALL_LABELS and ALL_MILESTONES passed as JSON to JS
- Double-click on empty column area opens create modal
  (pre-fills with that column name in title placeholder context)
- '+ Issue' button also opens the same modal
- Removed old inline new-issue-form (Alpine x-show), replaced with modal
- Create modal: title, body, multi-label checkboxes, milestone, assignee
- Edit form: title, body, multi-label checkboxes, assignee
- CSS: .label-checkboxes + .label-cb styles
This commit is contained in:
2026-07-08 10:47:54 -04:00
parent 3e78d4343c
commit ae41937b03
2 changed files with 217 additions and 157 deletions
+214 -157
View File
@@ -6,8 +6,7 @@
repo: '{{ repo }}',
milestone: '{{ current_milestone }}',
label: '{{ current_label }}',
assignee: '{{ current_assignee }}',
showNewIssue: false
assignee: '{{ current_assignee }}'
}">
<!-- Header -->
@@ -22,39 +21,10 @@
<div class="board-actions">
<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" @click="showNewIssue = !showNewIssue">+ Issue</button>
<button class="btn btn-sm btn-primary" onclick="openCreateModal('{{ owner }}','{{ repo }}','Backlog')">+ Issue</button>
</div>
</div>
<!-- New Issue Form -->
<div class="new-issue-form" x-show="showNewIssue" x-transition>
<h4>Nouvelle issue dans {{ owner }}/{{ repo }}</h4>
<form id="new-issue-form" onsubmit="handleNewIssue(event, '{{ owner }}', '{{ repo }}')">
<input type="hidden" name="csrf_token" class="csrf-token-field">
<input type="text" name="title" placeholder="Titre de l'issue..." required class="form-input">
<textarea name="body" placeholder="Description (markdown)..." rows="3" class="form-input"></textarea>
<div class="form-row">
<select name="labels" class="form-select">
<option value="">Label</option>
{% for lbl in labels %}
<option value="{{ lbl.id }}">🏷 {{ lbl.name }}</option>
{% endfor %}
</select>
<select name="milestone" class="form-select">
<option value="">Milestone</option>
{% for m in milestones %}
<option value="{{ m.id }}">📅 {{ m.title }}</option>
{% endfor %}
</select>
<input type="text" name="assignee" placeholder="Assigné à (login)" class="form-input" style="flex:1">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Créer</button>
<button type="button" class="btn" @click="showNewIssue = false">Annuler</button>
</div>
</form>
</div>
<!-- Filters -->
<div class="board-filters">
<select class="filter-select" name="milestone_filter"
@@ -87,9 +57,7 @@
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>
<button class="btn btn-sm" onclick="window.location.reload()">🔄 Rafraîchir</button>
</div>
<!-- Columns (fragment) -->
@@ -113,159 +81,243 @@
</div>
{% endif %}
</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>
// CSRF helper — set CSRF on all forms after load
function refreshCsrfFields() {
const csrf = getCSRFToken();
document.querySelectorAll('.csrf-token-field').forEach(f => f.value = csrf);
}
document.addEventListener('DOMContentLoaded', refreshCsrfFields);
// ── Labels data (passed from server) ──
const ALL_LABELS = {{ labels | tojson }};
const ALL_MILESTONES = {{ milestones | tojson }};
function getCSRFToken() {
const match = document.cookie.match(/csrf_token=([^;]+)/);
return match ? match[1] : '';
const m = document.cookie.match(/csrf_token=([^;]+)/);
return m ? m[1] : '';
}
async function handleNewIssue(evt, owner, repo) {
evt.preventDefault();
const form = evt.target;
const title = form.title.value.trim();
if (!title) return;
const body = form.body.value.trim();
const labels = form.labels.value;
const milestone = form.milestone.value;
const assignee = form.assignee.value.trim();
const csrf = getCSRFToken();
// ── 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}`;
const params = new URLSearchParams({ title, body, labels, milestone, assignee });
try {
const resp = await fetch(`/api/issues/${owner}/${repo}?${params}`, {
method: 'POST',
headers: { 'X-CSRF-Token': csrf }
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>';
});
if (resp.ok) {
window.location.reload();
} else {
alert('Erreur lors de la création');
}
} catch(e) {
alert('Erreur: ' + e);
}
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');
}
}
// Initialize SortableJS
// ── 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',
group: 'kanban', animation: 150, ghostClass: 'card-ghost',
onEnd: async function(evt) {
const card = evt.item;
const issueId = card.dataset.issueId;
const newColumn = evt.to.dataset.column;
const owner = evt.to.dataset.owner;
const repo = evt.to.dataset.repo;
const csrfToken = getCSRFToken();
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(newColumn)}`,
{ method: 'POST', headers: { 'X-CSRF-Token': csrfToken } }
);
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 cardCount = col.querySelectorAll('.card').length;
const badge = col.querySelector('.column-count');
if (badge) badge.textContent = cardCount;
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();
}
} 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>`; });
.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 || {};
let h = '';
// Edit mode toggle: store data for later
window._issueData = {owner, repo, issue, comments, checklists, card};
h += `<div class="detail-section"><span class="detail-state badge ${issue.state}">${issue.state}</span> <strong>${escHtml(issue.title||'')}</strong></div>`;
h += `<div class="detail-meta">`;
(issue.labels||[]).forEach(l => h += `<span class="card-label" style="background:#${l.color||'666'}33;color:#${l.color||'ccc'}">${escHtml(l.name)}</span>`);
if (issue.milestone) h += `<span>📅 ${escHtml(issue.milestone.title)}</span>`;
if (issue.assignee) h += `<span>👤 ${escHtml(issue.assignee.login)}</span>`;
h += `</div>`;
h += `<div class="detail-editable"><label>Priorité: <select onchange="updateCardMeta('${owner}','${repo}',${issue.number},this.value,'')" class="form-select-sm">`;
['Low','Medium','High','Urgent'].forEach(p => h += `<option value="${p}" ${(card.priority||'Medium')===p?'selected':''}>${p}</option>`);
h += `</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>`;
h += `<div class="detail-section"><h4>Description</h4><div class="markdown-body">${escHtml(issue.body||'')}</div></div>`;
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 tache..." class="form-input-sm" onkeydown="if(event.key==='Enter')addCheckItem(${cl.id},'${owner}','${repo}',${issue.number})">`;
h += `<button onclick="addCheckItem(${cl.id},'${owner}','${repo}',${issue.number})" class="btn btn-sm">+</button></div></div>`;
});
if (!checklists.length) h += `<button onclick="createChecklist('${owner}','${repo}',${issue.number})" class="btn btn-sm">+ Nouvelle checklist</button>`;
h += `</div>`;
h += `<div class="detail-section"><h4>Commentaires (${comments.length})</h4>`;
comments.forEach(c => h += `<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>`);
h += `</div>`;
h += `<div class="detail-actions"><a href="${issue.html_url||''}" target="_blank" class="btn btn-sm">🔗 Ouvrir dans Gitea</a>`;
h += `<button onclick="showEditForm('${owner}','${repo}',${issue.number})" class="btn btn-sm">✏️ Éditer</button>`;
h += `<button onclick="closeModal()" class="btn btn-sm">Fermer</button></div>`;
let h = '';
h += '<div class="detail-section"><span class="detail-state badge '+issue.state+'">'+issue.state+'</span> <strong>'+escHtml(issue.title||'')+'</strong></div>';
h += '<div class="detail-meta">';
(issue.labels||[]).forEach(l => h += '<span class="card-label" style="background:#'+(l.color||'666')+'33;color:#'+(l.color||'ccc')+'">'+escHtml(l.name)+'</span>');
if (issue.milestone) h += '<span>📅 '+escHtml(issue.milestone.title)+'</span>';
if (issue.assignee) h += '<span>👤 '+escHtml(issue.assignee.login)+'</span>';
h += '</div>';
h += '<div class="detail-editable"><label>Priorité: <select onchange="updateCardMeta(\''+owner+'\',\''+repo+'\','+issue.number+',this.value,\'\')" class="form-select-sm">';
['Low','Medium','High','Urgent'].forEach(p => h += '<option value="'+p+'" '+(card.priority||'Medium')===p?'selected':'')+'>'+p+'</option>');
h += '</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>';
h += '<div class="detail-section"><h4>Description</h4><div class="markdown-body">'+escHtml(issue.body||'')+'</div></div>';
h += renderChecklists(owner, repo, issue.number, checklists);
h += '<div class="detail-section"><h4>Commentaires ('+comments.length+')</h4>';
comments.forEach(c => h += '<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>');
h += '</div>';
h += '<div class="detail-actions"><a href="'+(issue.html_url||'')+'" target="_blank" class="btn btn-sm">🔗 Ouvrir dans Gitea</a>';
h += '<button onclick="showEditForm(\''+owner+'\',\''+repo+'\','+issue.number+')" class="btn btn-sm">✏️ Éditer</button>';
h += '<button onclick="closeModal()" class="btn btn-sm">Fermer</button></div>';
return h;
}
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 || {};
const issue = data.issue || {};
fetch(`/api/collaborators/${owner}/${repo}`).then(r=>r.json()).then(cdata => {
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 += '<div class="form-row">';
h += '<input type="text" id="edit-assignee" value="'+(issue.assignee?issue.assignee.login:'')+'" placeholder="Assigné à (login)" class="form-input" style="flex:1">';
h += '</div>';
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;
});
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) {
@@ -274,35 +326,40 @@
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(','));
await fetch(`/api/issues/${owner}/${repo}/${issueId}?${params}`, {
method: 'PATCH', headers: {'X-CSRF-Token': getCSRFToken()}
});
openCardDetail(owner, repo, issueId);
}
// ── Helpers ──
function escHtml(s) { return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
async function updateCardMeta(o,r,i,priority,dueDate) { await fetch(`/api/card/${o}/${r}/${i}?priority=${priority}&due_date=${dueDate}`,{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 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 parts=document.getElementById('card-modal-title').textContent.split(' — ');
const [ow,rp]=parts[1]?parts[1].split('/'):['',''];
if(ow&&rp) openCardDetail(ow,rp,parseInt(parts[0].replace('#','')));
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,issueId) {
const inp=document.getElementById(`new-item-${cid}`);
if(!inp||!inp.value.trim())return;
await fetch(`/api/checklist-items/${o}/${r}/${issueId}/${cid}?content=${encodeURIComponent(inp.value)}`,{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}});
openCardDetail(o,r,issueId);
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,issueId) { await fetch(`/api/checklists/${o}/${r}/${issueId}`,{method:'POST',headers:{'X-CSRF-Token':getCSRFToken()}}); openCardDetail(o,r,issueId); }
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 deja enregistre':'Webhook enregistre');
}catch(e){alert('Erreur: '+e);}
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); }
}
</script>
{% endblock %}
+3
View File
@@ -200,6 +200,9 @@ body { min-height: 100vh; }
}
.form-input:focus, .form-select:focus { outline: none; border-color: var(--accent); }
.form-label { font-size: 12px; color: var(--text-dim); display: block; margin: 8px 0 4px 0; }
.label-checkboxes { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 4px; }
.label-cb { display: flex; align-items: center; gap: 3px; font-size: 12px; cursor: pointer; }
.label-cb input[type="checkbox"] { cursor: pointer; }
.form-input-sm { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 4px 6px; color: var(--text); font-size: 12px; }
.form-select-sm { background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 2px 4px; color: var(--text); font-size: 12px; }
.form-row { display: flex; gap: 8px; }