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)
|
self._cache_put(key, milestones)
|
||||||
return 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(
|
async def create_issue(
|
||||||
self, owner: str, repo: str, title: str, body: str = ""
|
self, owner: str, repo: str, title: str, body: str = ""
|
||||||
) -> dict:
|
) -> 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:
|
def __init__(self) -> None:
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
self.starred = set()
|
||||||
self.repos = [
|
self.repos = [
|
||||||
_repo("demo", now - timedelta(days=1)),
|
_repo("demo", now - timedelta(days=1)),
|
||||||
_repo("vieux", now - timedelta(days=800)),
|
_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"):
|
async def get_milestones(self, owner, repo, state="open"):
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user