feat(0.33.0): filtrage professionnel sur Mes issues (/my)
CI / test (push) Successful in 2m26s

- Barre de filtres en carte : recherche texte (titre, projet,
  #numéro, étiquettes ; accents/casse ignorés, raccourci /),
  listes Projet et Étiquette avec compteurs, chips de
  colonnes colorées multi-sélection, 4 tris.
- Facettes calculées côté serveur ; filtrage instantané côté
  client (attributs data-*), compteur en direct, compteurs
  de section, état vide + réinitialisation.
- Filtres structurels persistés en localStorage.
- 281 tests verts (3 nouveaux, palier 22) ; vérifié en
  Chromium réel sur les vraies données (467 issues :
  recherche 'agent' -> 24 résultats instantanés).
This commit is contained in:
Spark
2026-10-26 10:20:37 -04:00
parent d419fe3086
commit 9fe35f6c8f
8 changed files with 368 additions and 7 deletions
+1 -1
View File
@@ -177,7 +177,7 @@ from urllib.parse import urlparse
BASE_DIR = Path(__file__).resolve().parent.parent
APP_VERSION = "0.32.2"
APP_VERSION = "0.33.0"
def _env_bool(name: str, default: bool) -> bool:
+35 -1
View File
@@ -4245,14 +4245,48 @@ async def my_issues(request: Request):
logger.debug("search failed for /my (conn %s): %s", cid, exc)
total = len(results)
groups = _group_issue_results(results)
# Facettes de filtrage (0.33.0) : compteurs calculés une
# fois côté serveur ; le filtrage lui-même est instantané
# côté client, sur des attributs data-* des lignes.
names = _ws_names(request)
order = _ws_keys(request)
repo_facets = [
{"full_name": g["full_name"], "count": len(g["rows"])}
for g in groups
]
col_counts: dict[str, int] = {}
label_counts: dict[str, int] = {}
for g in groups:
for row in g["rows"]:
col_counts[row["column"]] = col_counts.get(row["column"], 0) + 1
for label in row["issue"].get("labels") or []:
name = label.get("name") or ""
if name:
label_counts[name] = label_counts.get(name, 0) + 1
column_facets = [
{"key": key, "count": col_counts[key]}
for key in order
if key in col_counts
] + [
{"key": key, "count": count}
for key, count in col_counts.items()
if key not in order
]
label_facets = [
{"name": name, "count": label_counts[name]}
for name in sorted(label_counts, key=str.lower)
]
return render(
request,
"my.html",
user=user,
groups=groups,
total=total,
column_labels=_ws_names(request),
column_labels=names,
column_colors=_ws_colors(request),
repo_facets=repo_facets,
column_facets=column_facets,
label_facets=label_facets,
)
+56 -4
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mes issues — Kanbante</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>👤</text></svg>">
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="/static/css/style.css?v={{ version }}">
</head>
<body>
{% include "_header.html" %}
@@ -15,17 +15,68 @@
<div class="empty-state">🔑 Aucun utilisateur Gitea résolu : définissez <code>GITEA_TOKEN</code> pour voir les issues qui vous sont assignées.</div>
{% else %}
<p class="focus-intro">Issues ouvertes assignées à <strong>{{ user.login }}</strong>, tous projets confondus — {{ total }} au total.</p>
{% if groups %}
<div class="my-filters" id="my-filters">
<div class="my-filters-row">
<div class="my-search-wrap">
<span class="my-search-icon" aria-hidden="true">🔍</span>
<input type="search" id="my-search" autocomplete="off"
placeholder="Rechercher un titre, un projet, un #numéro, une étiquette… ( / )">
</div>
<select id="my-repo" title="Filtrer par projet">
<option value="">Tous les projets</option>
{% for f in repo_facets %}
<option value="{{ f.full_name }}">{{ f.full_name }} ({{ f.count }})</option>
{% endfor %}
</select>
<select id="my-label" title="Filtrer par étiquette">
<option value="">Toutes les étiquettes</option>
{% for f in label_facets %}
<option value="{{ f.name }}">{{ f.name }} ({{ f.count }})</option>
{% endfor %}
</select>
<select id="my-sort" title="Trier les résultats">
<option value="recent">Activité récente d'abord</option>
<option value="oldest">Plus anciennes d'abord</option>
<option value="title">Titre (A → Z)</option>
<option value="repo">Projet (A → Z)</option>
</select>
</div>
<div class="my-filters-row my-filters-meta">
<div class="my-col-chips" id="my-col-chips" role="group" aria-label="Filtrer par colonne">
<button type="button" class="filter-chip on" data-col="">Toutes les colonnes</button>
{% for f in column_facets %}
<button type="button" class="filter-chip" data-col="{{ f.key }}" aria-pressed="false">
<span class="filter-chip-dot" style="background: {{ column_colors[f.key] }}"></span>{{ column_labels[f.key] }} <span class="filter-chip-count">{{ f.count }}</span>
</button>
{% endfor %}
</div>
<span class="my-count" id="my-count" role="status"></span>
<button type="button" id="my-reset" class="btn-sm" hidden>↺ Réinitialiser les filtres</button>
</div>
</div>
{% endif %}
<div class="empty-state" id="my-empty" hidden>🔍 Aucune issue ne correspond à ces filtres.
<button type="button" id="my-empty-reset" class="btn-sm">↺ Réinitialiser les filtres</button>
</div>
{% if not groups %}
<div class="empty-state">🎉 Aucune issue ne vous est assignée pour le moment.</div>
{% endif %}
{% for group in groups %}
<section class="focus-group">
<section class="focus-group" data-repo="{{ group.full_name }}">
<h2><a href="/board/{{ group.full_name }}">{{ group.full_name }}</a>
<span class="badge secondary">{{ group.rows|length }}</span></h2>
<span class="badge secondary" data-group-count>{{ group.rows|length }}</span></h2>
<div class="issue-results">
{% for row in group.rows %}
{% set issue = row.issue %}
<div class="issue-row">
<div class="issue-row"
data-column="{{ row.column }}"
data-labels="{{ (issue.labels or [])|map(attribute='name')|join(' ') }}"
data-updated="{{ issue.updated_at or '' }}"
data-title="{{ issue.title }}"
data-search="#{{ issue.number }} {{ issue.title }} {{ group.full_name }} {{ (issue.labels or [])|map(attribute='name')|join(' ') }}">
<span class="issue-state">🟢</span>
<a class="issue-main" href="/board/{{ group.full_name }}">
<span class="card-id">#{{ issue.number }}</span> {{ issue.title }}
@@ -46,6 +97,7 @@
<footer>
Kanbante v{{ version }} · mes issues
</footer>
<script src="/static/js/my.js?v={{ version }}" defer></script>
<script src="/static/js/live.js" defer></script>
{% include "_palette.html" %}
<script src="/static/js/palette.js" defer></script>