/** * 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 = '🗂️ ' + owner + ' (' + byOwner[owner].length + ')▾'; 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). */ })();