- Dashboard projets Gitea (user + orgs) - Board Kanban 5 colonnes avec drag & drop (SortableJS) - Sync bidirectionnelle colonnes ↔ labels Gitea - Filtres milestone/label/assignee - Notes Markdown par projet - API REST (/api/health, /api/move, /api/col-mapping…) - Colonnes custom, WIP limits - Thème clair/sombre - Cache Gitea TTL - Docker + docker-compose - Tests pytest - Docs: README, ARCHITECTURE, CHANGELOG, ROADMAP
142 lines
5.3 KiB
HTML
142 lines
5.3 KiB
HTML
{% 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">
|
|
<a href="/notes/{{ owner }}/{{ repo }}" class="btn btn-sm">📝 Notes</a>
|
|
<a href="{{ settings.gitea_url }}/{{ owner }}/{{ repo }}/issues/new" target="_blank" class="btn btn-sm btn-primary">+ Issue</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="board-filters">
|
|
<select x-model="milestone" @change="window.location='?milestone='+milestone+'&label='+label+'&assignee='+assignee" class="filter-select">
|
|
<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 x-model="label" @change="window.location='?milestone='+milestone+'&label='+label+'&assignee='+assignee" class="filter-select">
|
|
<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>
|
|
</div>
|
|
|
|
<!-- Columns -->
|
|
<div class="board-columns"
|
|
hx-trigger="cardMoved from:body"
|
|
hx-get="/board/{{ owner }}/{{ repo }}?milestone={{ current_milestone }}&label={{ current_label }}&assignee={{ current_assignee }}"
|
|
hx-target=".board-columns"
|
|
hx-swap="outerHTML">
|
|
{% for col in columns %}
|
|
<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>
|
|
<div class="column-cards sortable"
|
|
data-column="{{ col }}"
|
|
data-owner="{{ owner }}"
|
|
data-repo="{{ repo }}">
|
|
{% for issue in column_map[col] %}
|
|
<div class="card"
|
|
data-issue-id="{{ issue.number }}"
|
|
data-column="{{ col }}"
|
|
draggable="true">
|
|
<div class="card-labels">
|
|
{% for lbl in issue.labels %}
|
|
<span class="card-label" style="background:#{{ lbl.color or '666' }}33; color:#{{ lbl.color or 'ccc' }}">{{ lbl.name }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="card-title">
|
|
<a href="{{ issue.html_url }}" target="_blank">#{{ issue.number }}</a>
|
|
{{ issue.title[:80] }}{% if issue.title|length > 80 %}...{% endif %}
|
|
</div>
|
|
<div class="card-meta">
|
|
{% if issue.assignee %}
|
|
<span class="card-assignee" title="{{ issue.assignee.login }}">
|
|
<img src="{{ issue.assignee.avatar_url }}" alt="" width="16" height="16" class="avatar">
|
|
{{ issue.assignee.login }}
|
|
</span>
|
|
{% endif %}
|
|
{% if issue.milestone %}
|
|
<span class="card-milestone">📅 {{ issue.milestone.title }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if issue._card and issue._card.priority %}
|
|
<div class="card-priority priority-{{ issue._card.priority|lower }}">{{ issue._card.priority }}</div>
|
|
{% endif %}
|
|
{% if issue._card and issue._card.due_date %}
|
|
<div class="card-due {% if issue._card.due_date < now %}overdue{% endif %}">
|
|
⏰ {{ issue._card.due_date[:10] }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// ponytail: initialize SortableJS on each column
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
document.querySelectorAll('.sortable').forEach(el => {
|
|
new Sortable(el, {
|
|
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;
|
|
|
|
// Optimistic: move card in DOM is already done
|
|
|
|
// Persist to backend
|
|
try {
|
|
await fetch(`/api/move?owner=${owner}&repo=${repo}&issue_id=${issueId}&column=${encodeURIComponent(newColumn)}`, {
|
|
method: 'POST'
|
|
});
|
|
|
|
// Update column counts
|
|
document.querySelectorAll('.board-column').forEach(col => {
|
|
const count = col.querySelectorAll('.card').length;
|
|
const badge = col.querySelector('.column-count');
|
|
if (badge) badge.textContent = count;
|
|
});
|
|
} catch (e) {
|
|
console.error('Move failed:', e);
|
|
// ponytail: reload on error
|
|
window.location.reload();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|