feat(projets): épinglés par étoiles Gitea (#20)
list_starred/set_starred sur /user/starred (vérité dans la
forge), endpoint POST /api/projects/{o}/{r}/star (CSRF), section
Épinglés hors onglets, bouton étoile sur les cartes et bascule
sans rechargement; les épinglés sortent de leur grille d'onglet.
This commit is contained in:
@@ -317,6 +317,26 @@ class GiteaClient:
|
||||
self._cache_put(key, milestones)
|
||||
return milestones
|
||||
|
||||
async def list_starred(self) -> list[dict]:
|
||||
"""Repos starred by the token user (issue #20: pinning rides
|
||||
on Gitea stars — the forge stays the source of truth)."""
|
||||
key = "user:starred"
|
||||
cached = self._cache_get(key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
repos = await self._get_all_pages("/user/starred")
|
||||
self._cache_put(key, repos)
|
||||
return repos
|
||||
|
||||
async def set_starred(self, owner: str, repo: str, starred: bool) -> None:
|
||||
"""Star / unstar a repo for the token user."""
|
||||
if starred:
|
||||
r = await self.client.put(f"/user/starred/{owner}/{repo}")
|
||||
else:
|
||||
r = await self.client.delete(f"/user/starred/{owner}/{repo}")
|
||||
r.raise_for_status()
|
||||
self._cache_clear()
|
||||
|
||||
async def create_issue(
|
||||
self, owner: str, repo: str, title: str, body: str = ""
|
||||
) -> dict:
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Kanbante — pin projects via Gitea stars (issue #20).
|
||||
* The star lives in Gitea (source of truth); this only toggles it
|
||||
* through the API and moves the card between the pinned section
|
||||
* and its home tab grid, without a page reload.
|
||||
*/
|
||||
(function () {
|
||||
var pinnedSection = document.getElementById('pinned-section');
|
||||
var pinnedGrid = document.getElementById('pinned-grid');
|
||||
|
||||
function refreshPinnedVisibility() {
|
||||
if (!pinnedSection || !pinnedGrid) return;
|
||||
pinnedSection.hidden = pinnedGrid.querySelectorAll('.project-card').length === 0;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.star-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', async function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
var full = btn.getAttribute('data-full');
|
||||
var parts = full.split('/');
|
||||
var want = btn.getAttribute('data-starred') !== '1';
|
||||
btn.disabled = true;
|
||||
try {
|
||||
var fd = new FormData();
|
||||
fd.append('starred', want ? '1' : '0');
|
||||
fd.append('csrf_token', window.KANBANTE_CSRF || '');
|
||||
var res = await fetch('/api/projects/' + parts[0] + '/' + parts[1] + '/star', {
|
||||
method: 'POST', body: fd,
|
||||
});
|
||||
var data = await res.json();
|
||||
if (!data.ok) throw new Error(data.error || 'échec');
|
||||
btn.setAttribute('data-starred', want ? '1' : '0');
|
||||
btn.textContent = want ? '⭐' : '☆';
|
||||
btn.title = want ? 'Désépingler (étoile Gitea)' : 'Épingler (étoile Gitea)';
|
||||
var card = btn.closest('.project-card');
|
||||
if (card) {
|
||||
if (want && pinnedGrid) {
|
||||
pinnedGrid.appendChild(card);
|
||||
} else {
|
||||
var home = document.getElementById('grid-' + (card.getAttribute('data-home') || 'active'));
|
||||
if (home) home.appendChild(card);
|
||||
}
|
||||
refreshPinnedVisibility();
|
||||
}
|
||||
} catch (err) {
|
||||
btn.textContent = '⚠️';
|
||||
setTimeout(function () {
|
||||
btn.textContent = btn.getAttribute('data-starred') === '1' ? '⭐' : '☆';
|
||||
}, 1600);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -68,6 +68,7 @@ class FakeGitea:
|
||||
|
||||
def __init__(self) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
self.starred = set()
|
||||
self.repos = [
|
||||
_repo("demo", now - timedelta(days=1)),
|
||||
_repo("vieux", now - timedelta(days=800)),
|
||||
@@ -270,6 +271,15 @@ class FakeGitea:
|
||||
},
|
||||
]
|
||||
|
||||
async def list_starred(self):
|
||||
return [r for r in self.repos if r["full_name"] in self.starred]
|
||||
|
||||
async def set_starred(self, owner, repo, starred):
|
||||
if starred:
|
||||
self.starred.add(f"{owner}/{repo}")
|
||||
else:
|
||||
self.starred.discard(f"{owner}/{repo}")
|
||||
|
||||
async def get_milestones(self, owner, repo, state="open"):
|
||||
return [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user