Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7ddc10971 | ||
|
|
542ed70c1a | ||
|
|
52c41ead91 | ||
|
|
f40d4c817b | ||
|
|
71881514c9 |
@@ -1,5 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.2 — 2026-10-11
|
||||
|
||||
### Correctif
|
||||
- **L'indice « Glissez une carte ici » restait affiché après un
|
||||
dépôt dans une colonne vide** : il est rendu par le serveur pour
|
||||
les colonnes vides au chargement, et le glisser-déposer ne le
|
||||
retirait jamais du DOM. `refreshEmptyHints()` recalcule désormais
|
||||
l'indice d'après le contenu réel de chaque colonne après tout
|
||||
déplacement (et après un retour arrière en cas d'échec) : retiré
|
||||
dès qu'une carte est présente, rétabli quand la dernière carte
|
||||
part. SortableJS est en outre restreint aux seules cartes
|
||||
(`draggable: '.kanban-card'`), l'indice n'est plus un élément
|
||||
déplaçable parasite.
|
||||
|
||||
## 0.5.1 — 2026-10-11
|
||||
|
||||
### Correctif critique
|
||||
- **Les boards étaient vides contre un vrai Gitea** : l'API Gitea
|
||||
(1.27.3 vérifié) renvoie la clé `"pull_request": null` sur les
|
||||
issues ordinaires ; le filtre `_is_issue` testait la *présence* de
|
||||
la clé (`"pull_request" not in item`), donc chaque vraie issue
|
||||
était classée « pull request » et écartée. Board, vue Focus et
|
||||
briefing ne montraient aucune carte (le dashboard, qui lit les
|
||||
compteurs des dépôts, semblait normal). Corrigé : le filtre teste
|
||||
la *valeur* (`not item.get("pull_request")`).
|
||||
- Le faux Gitea des tests omettait la clé, ce qui masquait le bug :
|
||||
il reproduit désormais fidèlement le payload réel (clé toujours
|
||||
présente, `null` sur les issues), plus 4 tests de régression dédiés
|
||||
(88 tests au total). Smoke test réel : le board `bruno/flowdeck`
|
||||
passe de 0 à 6 cartes, dont l'issue de test « test d'ajout ! ».
|
||||
|
||||
## 0.5.0 — 2026-10-11
|
||||
|
||||
« Palier 3 » (points 7 à 9) + board enrichi.
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
APP_VERSION = "0.5.0"
|
||||
APP_VERSION = "0.5.2"
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool) -> bool:
|
||||
|
||||
+4
-2
@@ -172,8 +172,10 @@ class GiteaClient:
|
||||
) -> list[dict]:
|
||||
"""Get issues for a repo (paginated, cached).
|
||||
|
||||
Note: Gitea's issues endpoint also returns pull requests; each
|
||||
PR carries a "pull_request" key and callers filter on it.
|
||||
Note: Gitea's issues endpoint also returns pull requests; a
|
||||
PR carries a non-null "pull_request" object, a plain issue
|
||||
carries "pull_request": null — callers filter on truthiness
|
||||
(see app.main._is_issue).
|
||||
"""
|
||||
key = f"issues:{owner}/{repo}:{state}"
|
||||
cached = self._cache_get(key)
|
||||
|
||||
+9
-2
@@ -323,8 +323,15 @@ def classify_repos(
|
||||
|
||||
|
||||
def _is_issue(item: dict) -> bool:
|
||||
"""Gitea's issues endpoint also returns PRs; they carry this key."""
|
||||
return "pull_request" not in item
|
||||
"""True for issues, False for pull requests.
|
||||
|
||||
Gitea's issues endpoint also returns PRs. A PR carries a
|
||||
``pull_request`` *object*; a plain issue carries the same key
|
||||
with value ``null`` (verified against Gitea 1.27.3). Testing key
|
||||
presence therefore classifies every real issue as a PR and
|
||||
empties the board — test the value's truthiness instead.
|
||||
"""
|
||||
return not item.get("pull_request")
|
||||
|
||||
|
||||
def _merge_issues_with_board(
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "kanbante"
|
||||
version = "0.5.0"
|
||||
version = "0.5.2"
|
||||
description = "Kanban project manager integrated with Gitea"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
|
||||
@@ -28,12 +28,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
new Sortable(col, {
|
||||
group: 'kanban',
|
||||
animation: 200,
|
||||
draggable: '.kanban-card',
|
||||
ghostClass: 'dragging',
|
||||
onStart: function () { window.__kanbanteDragging = true; },
|
||||
dragClass: 'dragging',
|
||||
onEnd: function (evt) {
|
||||
window.__kanbanteDragging = false;
|
||||
if (evt.from === evt.to && evt.oldIndex === evt.newIndex) return;
|
||||
refreshEmptyHints();
|
||||
const card = evt.item;
|
||||
const formData = new FormData();
|
||||
formData.append('repo_full_name', card.dataset.repo);
|
||||
@@ -69,6 +71,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
.catch(err => {
|
||||
console.error('Move failed:', err);
|
||||
revert(evt);
|
||||
refreshEmptyHints();
|
||||
showToast('❌ Échec du déplacement — carte replacée', true);
|
||||
});
|
||||
}
|
||||
@@ -115,6 +118,25 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// The "Glissez une carte ici" hint is rendered by the server only
|
||||
// for columns empty at page load. After a drag, the DOM is the
|
||||
// truth: remove the hint from columns that hold cards, restore it
|
||||
// on columns that became empty. (Bug reported 2026-10-11: the
|
||||
// hint stayed forever after dropping a card into an empty column.)
|
||||
function refreshEmptyHints() {
|
||||
document.querySelectorAll('.column-cards').forEach(list => {
|
||||
const hint = list.querySelector('.column-empty');
|
||||
const hasCards = !!list.querySelector('.kanban-card');
|
||||
if (hasCards && hint) hint.remove();
|
||||
if (!hasCards && !hint) {
|
||||
const p = document.createElement('p');
|
||||
p.className = 'column-empty';
|
||||
p.textContent = 'Glissez une carte ici';
|
||||
list.appendChild(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateCounts() {
|
||||
document.querySelectorAll('.kanban-column').forEach(col => {
|
||||
const count = col.querySelectorAll('.kanban-card').length;
|
||||
|
||||
+3
-2
@@ -55,9 +55,10 @@ def _issue(number: int, title: str, state: str = "open", pr: bool = False) -> di
|
||||
"created_at": "2026-09-01T09:00:00Z",
|
||||
"updated_at": "2026-09-02T09:00:00Z",
|
||||
"html_url": f"http://gitea.test/Projets/demo/issues/{number}",
|
||||
# Faithful to real Gitea 1.27 payloads: the key is ALWAYS
|
||||
# present — null for a plain issue, an object for a PR.
|
||||
"pull_request": {"url": "http://gitea.test/pr"} if pr else None,
|
||||
}
|
||||
if pr:
|
||||
issue["pull_request"] = {"url": "http://gitea.test/pr"}
|
||||
return issue
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Regression: real Gitea returns "pull_request": null on plain issues.
|
||||
|
||||
The 0.2.0–0.5.0 filter tested key *presence* ("pull_request" not in
|
||||
item), which classified every real issue as a PR and silently emptied
|
||||
every board against a real Gitea — while the test fake omitted the
|
||||
key entirely, so the suite stayed green. Found 2026-10-11 after a
|
||||
user report ("new issue does not appear on the board"): the live
|
||||
flowdeck board rendered 0 cards for 6 open issues.
|
||||
"""
|
||||
|
||||
from app.main import _is_issue
|
||||
|
||||
|
||||
def test_plain_issue_with_null_pull_request_is_an_issue():
|
||||
assert _is_issue({"number": 1, "pull_request": None}) is True
|
||||
|
||||
|
||||
def test_pull_request_object_is_not_an_issue():
|
||||
assert _is_issue({"number": 2, "pull_request": {"url": "http://x/pr/2"}}) is False
|
||||
|
||||
|
||||
def test_missing_key_is_an_issue():
|
||||
assert _is_issue({"number": 3}) is True
|
||||
|
||||
|
||||
def test_board_renders_cards_from_realistic_payload(client):
|
||||
# The shared fake now mirrors the real payload (key always
|
||||
# present, null on plain issues): if the filter regresses to a
|
||||
# presence test, these cards vanish and this test goes red.
|
||||
r = client.get("/board/Projets/demo")
|
||||
assert r.status_code == 200
|
||||
assert "Tâche un" in r.text
|
||||
assert "Tâche focus" in r.text
|
||||
assert "PR déguisée" not in r.text
|
||||
Reference in New Issue
Block a user