Files
flowdeck/app/templates/board.html
T
bruno 336c7d8dfe feat: FlowDeck v0.2.0 — Kanban Gitea intégré
- 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
2026-07-08 08:41:31 -04:00

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 %}