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).
191 lines
7.2 KiB
JavaScript
191 lines
7.2 KiB
JavaScript
// Mes issues — filtrage instantané côté client (0.33.0).
|
|
// Aucune requête réseau : les lignes portent leurs facettes en
|
|
// attributs data-*, le moteur ne fait que comparer des chaînes
|
|
// normalisées. Les filtres structurels (projet, colonnes,
|
|
// étiquette, tri) persistent en localStorage ; la recherche
|
|
// texte, elle, est éphémère.
|
|
(() => {
|
|
const search = document.getElementById('my-search');
|
|
if (!search) return; // page sans résultats : rien à filtrer
|
|
const repoSel = document.getElementById('my-repo');
|
|
const labelSel = document.getElementById('my-label');
|
|
const sortSel = document.getElementById('my-sort');
|
|
const chipsBox = document.getElementById('my-col-chips');
|
|
const countEl = document.getElementById('my-count');
|
|
const resetBtn = document.getElementById('my-reset');
|
|
const emptyBox = document.getElementById('my-empty');
|
|
const emptyReset = document.getElementById('my-empty-reset');
|
|
const STORE_KEY = 'kanbante-my-filters';
|
|
|
|
const norm = (s) => (s || '')
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '');
|
|
|
|
const state = {
|
|
q: '',
|
|
repo: '',
|
|
label: '',
|
|
cols: new Set(),
|
|
sort: 'recent',
|
|
};
|
|
|
|
// ── Persistance des filtres structurels ────────────────
|
|
try {
|
|
const saved = JSON.parse(localStorage.getItem(STORE_KEY) || '{}');
|
|
if (saved.repo) state.repo = saved.repo;
|
|
if (saved.label) state.label = saved.label;
|
|
if (Array.isArray(saved.cols)) state.cols = new Set(saved.cols);
|
|
if (saved.sort) state.sort = saved.sort;
|
|
} catch { /* stockage indisponible : état par défaut */ }
|
|
|
|
function persist() {
|
|
try {
|
|
localStorage.setItem(STORE_KEY, JSON.stringify({
|
|
repo: state.repo,
|
|
label: state.label,
|
|
cols: [...state.cols],
|
|
sort: state.sort,
|
|
}));
|
|
} catch { /* sans gravité */ }
|
|
}
|
|
|
|
// Refléter l'état restauré dans les contrôles.
|
|
if (state.repo) repoSel.value = state.repo;
|
|
if (state.label) labelSel.value = state.label;
|
|
sortSel.value = state.sort;
|
|
// Un filtre restauré peut ne plus exister (projet disparu) :
|
|
// dans ce cas le select retombe à « tous » tout seul.
|
|
if (repoSel.value !== state.repo) state.repo = repoSel.value;
|
|
if (labelSel.value !== state.label) state.label = labelSel.value;
|
|
|
|
const groups = [...document.querySelectorAll('.focus-group')];
|
|
const allRows = [...document.querySelectorAll('.issue-row')];
|
|
allRows.forEach((row) => {
|
|
row._search = norm(row.dataset.search);
|
|
row._labels = norm(row.dataset.labels).split(' ').filter(Boolean);
|
|
row._title = norm(row.dataset.title);
|
|
});
|
|
|
|
function syncChips() {
|
|
chipsBox.querySelectorAll('.filter-chip').forEach((chip) => {
|
|
const col = chip.dataset.col;
|
|
const on = col === '' ? state.cols.size === 0 : state.cols.has(col);
|
|
chip.classList.toggle('on', on);
|
|
if (col !== '') chip.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
});
|
|
}
|
|
|
|
function sortRows() {
|
|
const mode = state.sort;
|
|
if (mode === 'repo') {
|
|
const main = document.querySelector('.focus-main');
|
|
[...groups].sort((a, b) =>
|
|
a.dataset.repo.localeCompare(b.dataset.repo)
|
|
).forEach((g) => main.appendChild(g));
|
|
}
|
|
groups.forEach((group) => {
|
|
const box = group.querySelector('.issue-results');
|
|
const rows = [...box.querySelectorAll('.issue-row')];
|
|
const by = {
|
|
recent: (a, b) => (b.dataset.updated || '').localeCompare(a.dataset.updated || ''),
|
|
oldest: (a, b) => (a.dataset.updated || '').localeCompare(b.dataset.updated || ''),
|
|
title: (a, b) => a._title.localeCompare(b._title),
|
|
repo: (a, b) => a._title.localeCompare(b._title),
|
|
}[mode];
|
|
rows.sort(by).forEach((r) => box.appendChild(r));
|
|
});
|
|
}
|
|
|
|
function apply() {
|
|
const q = norm(state.q).trim();
|
|
let visible = 0;
|
|
groups.forEach((group) => {
|
|
let groupVisible = 0;
|
|
group.querySelectorAll('.issue-row').forEach((row) => {
|
|
const okText = !q || row._search.includes(q);
|
|
const okRepo = !state.repo || group.dataset.repo === state.repo;
|
|
const okCol = state.cols.size === 0 || state.cols.has(row.dataset.column);
|
|
const okLabel = !state.label
|
|
|| row._labels.includes(norm(state.label));
|
|
const ok = okText && okRepo && okCol && okLabel;
|
|
row.hidden = !ok;
|
|
if (ok) groupVisible++;
|
|
});
|
|
group.hidden = groupVisible === 0;
|
|
const badge = group.querySelector('[data-group-count]');
|
|
if (badge) badge.textContent = groupVisible;
|
|
visible += groupVisible;
|
|
});
|
|
countEl.textContent = visible === allRows.length
|
|
? `${visible} issue${visible > 1 ? 's' : ''}`
|
|
: `${visible} sur ${allRows.length} issues`;
|
|
const filtering = q || state.repo || state.label
|
|
|| state.cols.size > 0;
|
|
resetBtn.hidden = !filtering;
|
|
emptyBox.hidden = visible !== 0;
|
|
syncChips();
|
|
persist();
|
|
}
|
|
|
|
function resetAll() {
|
|
state.q = '';
|
|
search.value = '';
|
|
state.repo = '';
|
|
repoSel.value = '';
|
|
state.label = '';
|
|
labelSel.value = '';
|
|
state.cols.clear();
|
|
sortSel.value = 'recent';
|
|
state.sort = 'recent';
|
|
sortRows();
|
|
apply();
|
|
search.focus();
|
|
}
|
|
|
|
// ── Événements ─────────────────────────────────────────
|
|
let raf = 0;
|
|
search.addEventListener('input', () => {
|
|
cancelAnimationFrame(raf);
|
|
raf = requestAnimationFrame(() => {
|
|
state.q = search.value;
|
|
apply();
|
|
});
|
|
});
|
|
search.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
search.value = '';
|
|
state.q = '';
|
|
apply();
|
|
}
|
|
});
|
|
repoSel.addEventListener('change', () => { state.repo = repoSel.value; apply(); });
|
|
labelSel.addEventListener('change', () => { state.label = labelSel.value; apply(); });
|
|
sortSel.addEventListener('change', () => {
|
|
state.sort = sortSel.value;
|
|
sortRows();
|
|
apply();
|
|
});
|
|
chipsBox.addEventListener('click', (e) => {
|
|
const chip = e.target.closest('.filter-chip');
|
|
if (!chip) return;
|
|
const col = chip.dataset.col;
|
|
if (col === '') state.cols.clear();
|
|
else if (state.cols.has(col)) state.cols.delete(col);
|
|
else state.cols.add(col);
|
|
apply();
|
|
});
|
|
resetBtn.addEventListener('click', resetAll);
|
|
emptyReset?.addEventListener('click', resetAll);
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key !== '/') return;
|
|
const tag = document.activeElement?.tagName;
|
|
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;
|
|
e.preventDefault();
|
|
search.focus();
|
|
});
|
|
|
|
sortRows();
|
|
apply();
|
|
})();
|