f04876658f4ae0ec7c633895c21f153975a2a506 max Tue Sep 15 05:48:42 2026 -0700 faceted composite: default sort column, restored drag order, and two smaller things, refs #38353 defaultSortCol was still 1, which the drag-handle column took over when manual row ordering went in. A composite with no defaultSortField opened sorted by a hidden column, with no sort arrow on any visible heading. Fixing that alone exposed the next one: coming back on the Active tab called setFilterMode(), which renumbers the order field from whatever is on screen, and that threw away the hand- dragged order just read back from localStorage. It was invisible only because the current order happened to be the drag order while the default sort column was the drag column. The restore now keeps the saved order; a click still renumbers, which is what a click means. Saved state had no eviction, so once the quota filled, the QuotaExceededError was swallowed and from then on no faceted composite on the origin could save anything again. Each entry now carries a timestamp and a full store drops the least recently written entry belonging to another composite and tries again. Narrowing a facet turned the track container back on even with nothing selected, which brings back an empty image. diff --git src/hg/js/facetedComposite.js src/hg/js/facetedComposite.js index 3ccac1a4448..587ca5a2bea 100644 --- src/hg/js/facetedComposite.js +++ src/hg/js/facetedComposite.js @@ -211,36 +211,83 @@ return (state && typeof state === "object") ? state : {}; } catch (e) { return {}; } } // A page length the user picked wins. Otherwise paginating a table that // would nearly fit anyway just hides rows behind a menu, so show everything // up to the first menu step past 25. -1 is what DataTables reads as "all". function savedPageLength(saved, rowCount) { if (typeof saved === "number" && saved !== 0) return saved; return rowCount < 50 ? -1 : 25; } + // Drop the least recently written saved state belonging to some *other* + // faceted composite, to make room. Returns false when there is nothing + // left to drop, which is the caller's signal to give up. + function evictOtherUiState() { + try { + let oldestKey = null, oldestTime = Infinity; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (!key || !key.startsWith("facetedComposite.") || key === uiStateKey) + continue; + let saved = 0; + try { + // An entry we cannot read is the first one to go. + saved = (JSON.parse(localStorage.getItem(key)) || {}).saved || 0; + } catch (e) { + saved = 0; + } + if (saved < oldestTime) { + oldestTime = saved; + oldestKey = key; + } + } + if (oldestKey === null) + return false; + localStorage.removeItem(oldestKey); + return true; + } catch (e) { + return false; + } + } + function saveUiState(patch) { + // The timestamp is what makes eviction above "least recently written" + // rather than arbitrary. + const write = () => localStorage.setItem( + uiStateKey, + JSON.stringify(Object.assign(loadUiState(), patch, {saved: Date.now()}))); try { - localStorage.setItem(uiStateKey, - JSON.stringify(Object.assign(loadUiState(), patch))); + write(); } catch (e) { - /* private window, or the quota is full; the page works without it */ + // Out of quota. A hand-dragged order is one id per row, so a few + // large composites can fill the store between them; without the + // retry below the first one to hit the limit would leave every + // faceted composite on this origin silently unable to save again. + while (evictOtherUiState()) { + try { + write(); + return; + } catch (e2) { + /* still no room; drop another and try again */ + } + } + /* private window, or nothing left to evict; the page works without it */ } } function generateHTML() { const container = document.createElement("div"); container.id = "myTag"; container.innerHTML = `