fix: positionnement des cartes selon les labels et mise à jour après édition
CI / test (push) Failing after 8s
CI / lint (push) Failing after 4s

- create_issue: utilise _issue_column() au lieu de hardcoder 'Backlog'
- update_issue_api: recalcule la colonne après changement de labels
- update_issue_api: utilise l'endpoint PUT /labels au lieu de PATCH (Gitea ignore labels dans PATCH)
- webhooks: utilise _issue_column() pour 'opened'/'reopened'
- saveIssueEdit: rafraîchit le board (HTMX) après édition au lieu de rouvrir la modale
- update_issue_labels: corrigé pour utiliser PUT /issues/{id}/labels (liste d'entiers)
This commit is contained in:
2026-07-08 11:27:56 -04:00
parent 6ca376a675
commit 7b2fa01b0a
4 changed files with 59 additions and 16 deletions
+34 -8
View File
@@ -10,6 +10,7 @@ from fastapi import APIRouter, HTTPException, Query, Request
from app.config import settings from app.config import settings
from app.db import get_conn from app.db import get_conn
from app.routers.board import _issue_column
from app.services.gitea_client import gitea from app.services.gitea_client import gitea
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -266,16 +267,18 @@ async def create_issue(
labels=label_ids, milestone=milestone_id, assignee=assignee, labels=label_ids, milestone=milestone_id, assignee=assignee,
) )
# Add card to local board # Add card to local board — determine column from label mapping
with get_conn() as conn: with get_conn() as conn:
board = conn.execute( board_row = conn.execute(
"SELECT id FROM boards WHERE project_owner=? AND project_name=?", "SELECT id, columns_json FROM boards WHERE project_owner=? AND project_name=?",
(owner, repo), (owner, repo),
).fetchone() ).fetchone()
if board: if board_row:
columns = json.loads(board_row["columns_json"])
col = _issue_column(issue, columns, board_row["id"])
conn.execute( conn.execute(
"INSERT INTO cards (board_id, gitea_issue_id, column_name, position) VALUES (?, ?, 'Backlog', 0)", "INSERT INTO cards (board_id, gitea_issue_id, column_name, position) VALUES (?, ?, ?, 0)",
(board["id"], issue["number"]), (board_row["id"], issue["number"], col),
) )
conn.commit() conn.commit()
@@ -300,6 +303,7 @@ async def update_issue_api(
raise HTTPException(status_code=429, detail="Rate limit exceeded") raise HTTPException(status_code=429, detail="Rate limit exceeded")
kwargs = {} kwargs = {}
label_ids = None
if title: if title:
kwargs["title"] = title kwargs["title"] = title
if body: if body:
@@ -307,13 +311,35 @@ async def update_issue_api(
if state: if state:
kwargs["state"] = state kwargs["state"] = state
if labels: if labels:
kwargs["labels"] = labels.split(",") label_ids = [int(l) for l in labels.split(",") if l.strip().isdigit()]
if milestone and milestone.strip().isdigit(): if milestone and milestone.strip().isdigit():
kwargs["milestone"] = int(milestone) kwargs["milestone"] = int(milestone)
if assignee: if assignee:
kwargs["assignees"] = [assignee] kwargs["assignees"] = [assignee]
issue = await gitea.update_issue(owner, repo, issue_id, **kwargs) if kwargs:
await gitea.update_issue(owner, repo, issue_id, **kwargs)
if label_ids is not None:
await gitea.update_issue_labels(owner, repo, issue_id, label_ids)
# Re-fetch issue to get complete updated state for column recalculation
issue = await gitea.get_issue(owner, repo, issue_id)
# Recalculate card column based on updated labels
with get_conn() as conn:
board_row = conn.execute(
"SELECT id, columns_json FROM boards WHERE project_owner=? AND project_name=?",
(owner, repo),
).fetchone()
if board_row:
columns = json.loads(board_row["columns_json"])
col = _issue_column(issue, columns, board_row["id"])
conn.execute(
"UPDATE cards SET column_name=?, updated_at=CURRENT_TIMESTAMP WHERE board_id=? AND gitea_issue_id=?",
(col, board_row["id"], issue_id),
)
conn.commit()
return {"status": "ok", "issue": issue} return {"status": "ok", "issue": issue}
+5 -3
View File
@@ -10,6 +10,7 @@ from fastapi import APIRouter, Request, HTTPException
from app.config import settings from app.config import settings
from app.db import get_conn from app.db import get_conn
from app.routers.board import _issue_column
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
router = APIRouter(tags=["webhooks"], prefix="/api/webhook") router = APIRouter(tags=["webhooks"], prefix="/api/webhook")
@@ -70,7 +71,7 @@ async def _handle_issue_event(payload: dict):
with get_conn() as conn: with get_conn() as conn:
board = conn.execute( board = conn.execute(
"SELECT id FROM boards WHERE project_owner=? AND project_name=?", "SELECT id, columns_json FROM boards WHERE project_owner=? AND project_name=?",
(owner, repo_name), (owner, repo_name),
).fetchone() ).fetchone()
@@ -79,8 +80,9 @@ async def _handle_issue_event(payload: dict):
board_id = board["id"] board_id = board["id"]
if action in ("opened", "reopened"): if action in ("opened", "reopened"):
# Add card to default column # Determine column from label mapping (fallback to first column)
column = "Backlog" columns = json.loads(board["columns_json"])
column = _issue_column(issue, columns, board_id)
conn.execute( conn.execute(
"""INSERT OR IGNORE INTO cards (board_id, gitea_issue_id, column_name, position) """INSERT OR IGNORE INTO cards (board_id, gitea_issue_id, column_name, position)
VALUES (?, ?, ?, 0)""", VALUES (?, ?, ?, 0)""",
+12 -3
View File
@@ -152,9 +152,18 @@ class GiteaClient:
return resp.json() return resp.json()
async def update_issue_labels( async def update_issue_labels(
self, owner: str, repo: str, issue_id: int, labels: list[str] self, owner: str, repo: str, issue_id: int, labels: list[int]
) -> dict: ) -> list[dict]:
return await self.update_issue(owner, repo, issue_id, labels=labels) """Replace all labels on an issue (PUT endpoint)."""
self._invalidate_issue_cache(owner, repo)
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.put(
f"{self._base}/repos/{owner}/{repo}/issues/{issue_id}/labels",
headers=self._headers,
json={"labels": labels},
)
resp.raise_for_status()
return resp.json()
async def close_issue(self, owner: str, repo: str, issue_id: int) -> dict: async def close_issue(self, owner: str, repo: str, issue_id: int) -> dict:
return await self.update_issue(owner, repo, issue_id, state="closed") return await self.update_issue(owner, repo, issue_id, state="closed")
+8 -2
View File
@@ -348,10 +348,16 @@
document.querySelectorAll('#card-modal-content input[name="edit-labels"]:checked').forEach(cb => checkedLabels.push(cb.value)); document.querySelectorAll('#card-modal-content input[name="edit-labels"]:checked').forEach(cb => checkedLabels.push(cb.value));
const params = new URLSearchParams({title, body, assignee}); const params = new URLSearchParams({title, body, assignee});
if (checkedLabels.length) params.set('labels', checkedLabels.join(',')); if (checkedLabels.length) params.set('labels', checkedLabels.join(','));
await fetch(`/api/issues/${owner}/${repo}/${issueId}?${params}`, { const resp = await fetch(`/api/issues/${owner}/${repo}/${issueId}?${params}`, {
method: 'PATCH', headers: {'X-CSRF-Token': getCSRFToken()} method: 'PATCH', headers: {'X-CSRF-Token': getCSRFToken()}
}); });
openCardDetail(owner, repo, issueId); if (resp.ok) {
closeModal();
// Refresh board columns to reposition card
htmx.ajax('GET', `/board/${owner}/${repo}?fragment=1`, {target: '#board-columns', swap: 'outerHTML'});
} else {
alert('Erreur lors de la mise à jour');
}
} }
// ── Helpers ── // ── Helpers ──