- tablesort.js : parseFloat lisait l'année des dates ISO (2026) et mettait toutes les lignes à égalité — comparaison numérique seulement pour les valeurs entièrement numériques. - dashboard.js : pinned-grid rejoint les grilles triées et regroupées (son exclusion rendait les deux outils invisibles là où l'œil se pose) ; dates comparées via Date.parse. - data-first-dir=desc sur Dernière activité / Écart / Zombies.
174 lines
7.3 KiB
JavaScript
174 lines
7.3 KiB
JavaScript
/**
|
|
* Kanbante — dashboard display tools (issues #21, #22, #23).
|
|
* Sort + group-by-owner for the card grids, sortable dense tables,
|
|
* and the per-card quick-actions menu. Preferences live in
|
|
* localStorage; the server markup stays the single source.
|
|
*/
|
|
(function () {
|
|
var LS_SORT = 'kanbante-dash-sort';
|
|
var LS_GROUP = 'kanbante-dash-group';
|
|
var LS_OWNERS = 'kanbante-dash-owners-closed';
|
|
|
|
function lsGet(key, fallback) {
|
|
try {
|
|
var v = localStorage.getItem(key);
|
|
return v === null ? fallback : v;
|
|
} catch (e) { return fallback; }
|
|
}
|
|
function lsSet(key, value) {
|
|
try { localStorage.setItem(key, value); } catch (e) { /* non-fatal */ }
|
|
}
|
|
|
|
/* ── Tri des grilles de cartes ─────────────────────────── */
|
|
var sortSelect = document.getElementById('dash-sort');
|
|
// 0.20.1 : la grille des Épinglés est incluse — l'exclure
|
|
// (choix de la 0.18.0) donnait l'impression que le tri et le
|
|
// regroupement « ne faisaient rien », la section épinglée
|
|
// étant celle qu'on regarde en premier.
|
|
var grids = ['pinned-grid', 'grid-active', 'grid-inactive', 'grid-archived']
|
|
.map(function (id) { return document.getElementById(id); })
|
|
.filter(Boolean);
|
|
|
|
function cardKey(card, mode) {
|
|
if (mode === 'name') return (card.getAttribute('data-name') || '').toLowerCase();
|
|
if (mode === 'open') return -(parseInt(card.getAttribute('data-open') || '0', 10));
|
|
if (mode === 'updated') return card.getAttribute('data-updated') || '';
|
|
if (mode === 'activity') return card.getAttribute('data-activity') || '';
|
|
return null; // 'default' : ordre serveur, on ne touche à rien
|
|
}
|
|
function sortCards(container, mode) {
|
|
var cards = Array.prototype.slice.call(
|
|
container.querySelectorAll('.project-card'));
|
|
if (mode === 'default' || !cards.length) return;
|
|
cards.sort(function (a, b) {
|
|
var ka = cardKey(a, mode), kb = cardKey(b, mode);
|
|
if (mode === 'updated' || mode === 'activity') {
|
|
// Comparaison en instants réels (les fuseaux des
|
|
// dates Gitea varient, -04:00/-05:00) ; une carte
|
|
// sans date coule en bas du tri décroissant.
|
|
var ta = Date.parse(ka), tb = Date.parse(kb);
|
|
if (isNaN(ta) && isNaN(tb)) return 0;
|
|
if (isNaN(ta)) return 1;
|
|
if (isNaN(tb)) return -1;
|
|
return tb - ta;
|
|
}
|
|
if (ka < kb) return mode === 'name' ? -1 : 1;
|
|
if (ka > kb) return mode === 'name' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
cards.forEach(function (c) { container.appendChild(c); });
|
|
}
|
|
function applySort() {
|
|
var mode = sortSelect ? sortSelect.value : 'default';
|
|
lsSet(LS_SORT, mode);
|
|
grids.forEach(function (grid) {
|
|
if (groupToggle && groupToggle.checked) {
|
|
grid.querySelectorAll('.owner-grid').forEach(function (og) {
|
|
sortCards(og, mode);
|
|
});
|
|
} else {
|
|
sortCards(grid, mode);
|
|
}
|
|
});
|
|
}
|
|
|
|
/* ── Regroupement par propriétaire ─────────────────────── */
|
|
var groupToggle = document.getElementById('dash-group');
|
|
function closedOwners() {
|
|
try { return JSON.parse(lsGet(LS_OWNERS, '[]')); } catch (e) { return []; }
|
|
}
|
|
function setOwnerClosed(owner, closed) {
|
|
var list = closedOwners().filter(function (o) { return o !== owner; });
|
|
if (closed) list.push(owner);
|
|
lsSet(LS_OWNERS, JSON.stringify(list));
|
|
}
|
|
function groupGrid(grid) {
|
|
var cards = Array.prototype.slice.call(
|
|
grid.querySelectorAll(':scope > .project-card'));
|
|
if (!cards.length) return;
|
|
var byOwner = {};
|
|
cards.forEach(function (c) {
|
|
var o = c.getAttribute('data-owner') || '—';
|
|
(byOwner[o] = byOwner[o] || []).push(c);
|
|
});
|
|
Object.keys(byOwner).sort().forEach(function (owner) {
|
|
var wrap = document.createElement('div');
|
|
wrap.className = 'owner-group';
|
|
var head = document.createElement('button');
|
|
head.type = 'button';
|
|
head.className = 'owner-head';
|
|
var closed = closedOwners().indexOf(owner) >= 0;
|
|
head.setAttribute('aria-expanded', closed ? 'false' : 'true');
|
|
head.innerHTML = '<span>🗂️ ' + owner + ' (' + byOwner[owner].length + ')</span><span class="owner-chevron">▾</span>';
|
|
var body = document.createElement('div');
|
|
body.className = 'project-grid owner-grid';
|
|
if (closed) body.hidden = true;
|
|
head.addEventListener('click', function () {
|
|
body.hidden = !body.hidden;
|
|
head.setAttribute('aria-expanded', body.hidden ? 'false' : 'true');
|
|
setOwnerClosed(owner, body.hidden);
|
|
});
|
|
byOwner[owner].forEach(function (c) { body.appendChild(c); });
|
|
wrap.appendChild(head);
|
|
wrap.appendChild(body);
|
|
grid.appendChild(wrap);
|
|
});
|
|
}
|
|
function ungroupGrid(grid) {
|
|
grid.querySelectorAll('.owner-group').forEach(function (wrap) {
|
|
var body = wrap.querySelector('.owner-grid');
|
|
Array.prototype.slice.call(body.querySelectorAll('.project-card'))
|
|
.forEach(function (c) { grid.appendChild(c); });
|
|
wrap.remove();
|
|
});
|
|
}
|
|
function applyGrouping() {
|
|
var on = !!(groupToggle && groupToggle.checked);
|
|
lsSet(LS_GROUP, on ? '1' : '0');
|
|
grids.forEach(function (grid) {
|
|
ungroupGrid(grid);
|
|
if (on) groupGrid(grid);
|
|
});
|
|
applySort();
|
|
}
|
|
|
|
if (sortSelect) {
|
|
sortSelect.value = lsGet(LS_SORT, 'default');
|
|
sortSelect.addEventListener('change', applySort);
|
|
}
|
|
if (groupToggle) {
|
|
groupToggle.checked = lsGet(LS_GROUP, '0') === '1';
|
|
groupToggle.addEventListener('change', applyGrouping);
|
|
}
|
|
applyGrouping(); // applique regroupement + tri mémorisés
|
|
|
|
/* ── Menu ⋯ d'actions rapides (#23) ────────────────────── */
|
|
function closeMenus() {
|
|
document.querySelectorAll('.card-menu').forEach(function (m) {
|
|
m.hidden = true;
|
|
});
|
|
document.querySelectorAll('[data-menu-btn]').forEach(function (b) {
|
|
b.setAttribute('aria-expanded', 'false');
|
|
});
|
|
}
|
|
document.querySelectorAll('[data-menu-btn]').forEach(function (btn) {
|
|
btn.addEventListener('click', function (e) {
|
|
e.stopPropagation();
|
|
var menu = btn.parentElement.querySelector('.card-menu');
|
|
var willOpen = menu.hidden;
|
|
closeMenus();
|
|
if (willOpen) {
|
|
menu.hidden = false;
|
|
btn.setAttribute('aria-expanded', 'true');
|
|
}
|
|
});
|
|
});
|
|
document.addEventListener('click', closeMenus);
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape') closeMenus();
|
|
});
|
|
|
|
/* Le tri des tableaux denses (#22) vit dans tablesort.js,
|
|
partagé avec /forecast et /health (0.19.0). */
|
|
})();
|