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
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user