From 90f2612f533042e5c9df89d98bff8140df860883 Mon Sep 17 00:00:00 2001 From: Piclaw Date: Wed, 15 Apr 2026 10:36:46 -0700 Subject: [PATCH 1/3] Add sort options to Contributing Open Issues page Add a 'Sort by' dropdown to the Open Issues tab on the /contributing page, allowing users to sort issues by: - Default (server-rendered order) - Newest first (fewest open days) - Oldest first (most open days) Sorting works by parsing the '(Open X days)' text already present in each issue title. Both issues within each library and the libraries themselves are sorted. Sort and filter selections are preserved in URL parameters and restored on page load / back button. Changes: - contributing/open_issues.html: Add sort dropdown, add id to label filter select for cleaner JS targeting - assets/javascript/contributing.js: Rewrite to support both label filtering and age-based sorting with original order restore - assets/sass/pages/_contributing.scss: Add flexbox layout for the filter/sort controls Fixes #645 --- assets/javascript/contributing.js | 191 ++++++++++++++++++++++++--- assets/sass/pages/_contributing.scss | 15 +++ contributing/open_issues.html | 34 +++-- 3 files changed, 212 insertions(+), 28 deletions(-) diff --git a/assets/javascript/contributing.js b/assets/javascript/contributing.js index 48d8bf1b58..f66929ca32 100644 --- a/assets/javascript/contributing.js +++ b/assets/javascript/contributing.js @@ -1,11 +1,21 @@ document.addEventListener('DOMContentLoaded', function() { // only load on open issues page for now - var issueSelect = document.querySelector(".open-issues select"); + var issueSelect = document.querySelector(".open-issues #label-filter"); if (!issueSelect) { return; } - issueSelect.onchange = issueSelectHandler; + issueSelect.onchange = function(event) { + issueSelectHandler(event); + applySorting(); + }; + + var sortSelect = document.querySelector(".open-issues #sort-order"); + if (sortSelect) { + sortSelect.onchange = function() { + applySorting(); + }; + } // load issues label when using back button window.addEventListener('popstate', loadIssues.bind(null, true)); @@ -17,19 +27,27 @@ document.addEventListener('DOMContentLoaded', function() { function loadIssues(isPopState) { var params = new URLSearchParams(window.location.search); var label = params.get('label'); + var sort = params.get('sort'); - if (!label) { - return; + if (sort) { + var sortSelect = document.querySelector('.open-issues #sort-order'); + if (sortSelect) { + sortSelect.value = sort; + } + } + + if (label) { + issueSelectHandler(label, isPopState); + var issuesList = document.querySelector('.open-issues #label-filter'); + issuesList.value = label; } - issueSelectHandler(label, isPopState); - var issuesList = document.querySelector('.open-issues select'); - issuesList.value = label; + applySorting(); } function issueSelectHandler(event, isPopState) { if (event.target) { - var selectedOption = this.options[this.selectedIndex].value; + var selectedOption = event.target.options[event.target.selectedIndex].value; } else { // page loads will set the event as just the selected label from params var selectedOption = event; @@ -37,7 +55,7 @@ function issueSelectHandler(event, isPopState) { // don't set params on the back button if (!isPopState) { - setIssueParams(selectedOption); + setParams(); } // hide all elements first @@ -47,17 +65,158 @@ function issueSelectHandler(event, isPopState) { }); // show the selected options - var selectedOption = selectedOption === 'all' ? 'li' : `.${selectedOption}`; - var items = document.querySelectorAll(`.issues-list ${selectedOption}`); + var selector = selectedOption === 'all' ? 'li' : '.' + selectedOption; + var items = document.querySelectorAll('.issues-list ' + selector); items.forEach(function(item) { - item.style.display = 'block' + item.style.display = 'block'; item.parentElement.closest('li').style.display = 'block'; }); } -function setIssueParams(label) { +function getIssueDays(element) { + // Parse "(Open X days)" from the issue title text + var text = element.textContent || ''; + var match = text.match(/\(Open\s+(\d+)\s+days?\)/i); + if (match) { + return parseInt(match[1], 10); + } + return 0; +} + +function applySorting() { + var sortSelect = document.querySelector('.open-issues #sort-order'); + if (!sortSelect) return; + + var sortOrder = sortSelect.value; + if (sortOrder === 'default') { + // Restore original order by reloading — but simpler to just not sort + // We store original order on first run + restoreOriginalOrder(); + setParams(); + return; + } + + // Sort issues within each library's issues-list + var issuesLists = document.querySelectorAll('.issues-list'); + issuesLists.forEach(function(list) { + var items = Array.from(list.querySelectorAll(':scope > li')); + + // Store original order if not already stored + if (!list.dataset.originalOrder) { + list.dataset.originalOrder = 'stored'; + items.forEach(function(item, index) { + item.dataset.originalIndex = index; + }); + } + + items.sort(function(a, b) { + var daysA = getIssueDays(a); + var daysB = getIssueDays(b); + if (sortOrder === 'newest') { + return daysA - daysB; // fewer days = newer = first + } else { + return daysB - daysA; // more days = older = first + } + }); + + // Re-append in sorted order + items.forEach(function(item) { + list.appendChild(item); + }); + }); + + // Also sort the library-level list items by their oldest/newest issue + var topList = document.querySelector('.open-issues > ul'); + if (topList) { + var libraryItems = Array.from(topList.querySelectorAll(':scope > li')); + + if (!topList.dataset.originalOrder) { + topList.dataset.originalOrder = 'stored'; + libraryItems.forEach(function(item, index) { + item.dataset.originalIndex = index; + }); + } + + libraryItems.sort(function(a, b) { + var issuesA = a.querySelectorAll('.issues-list > li'); + var issuesB = b.querySelectorAll('.issues-list > li'); + var maxA = getMaxDays(issuesA, sortOrder); + var maxB = getMaxDays(issuesB, sortOrder); + if (sortOrder === 'newest') { + return maxA - maxB; + } else { + return maxB - maxA; + } + }); + + libraryItems.forEach(function(item) { + topList.appendChild(item); + }); + } + + setParams(); +} + +function getMaxDays(issues, sortOrder) { + var result = sortOrder === 'newest' ? Infinity : 0; + issues.forEach(function(issue) { + var days = getIssueDays(issue); + if (sortOrder === 'newest') { + result = Math.min(result, days); + } else { + result = Math.max(result, days); + } + }); + return result === Infinity ? 0 : result; +} + +function restoreOriginalOrder() { + // Restore library-level order + var topList = document.querySelector('.open-issues > ul'); + if (topList && topList.dataset.originalOrder) { + var libraryItems = Array.from(topList.querySelectorAll(':scope > li')); + libraryItems.sort(function(a, b) { + return (parseInt(a.dataset.originalIndex) || 0) - (parseInt(b.dataset.originalIndex) || 0); + }); + libraryItems.forEach(function(item) { + topList.appendChild(item); + }); + } + + // Restore issue-level order within each list + var issuesLists = document.querySelectorAll('.issues-list'); + issuesLists.forEach(function(list) { + var items = Array.from(list.querySelectorAll(':scope > li')); + items.sort(function(a, b) { + return (parseInt(a.dataset.originalIndex) || 0) - (parseInt(b.dataset.originalIndex) || 0); + }); + items.forEach(function(item) { + list.appendChild(item); + }); + }); +} + +function setParams() { var params = new URLSearchParams(window.location.search); - params.set("label", label); - var newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?${params.toString()}`; - window.history.pushState({path:newUrl}, '', newUrl); + + var labelSelect = document.querySelector('.open-issues #label-filter'); + if (labelSelect && labelSelect.value && labelSelect.value !== 'all') { + params.set("label", labelSelect.value); + } else { + params.delete("label"); + } + + var sortSelect = document.querySelector('.open-issues #sort-order'); + if (sortSelect && sortSelect.value && sortSelect.value !== 'default') { + params.set("sort", sortSelect.value); + } else { + params.delete("sort"); + } + + var query = params.toString(); + var newUrl = window.location.protocol + '//' + window.location.host + window.location.pathname; + if (query) { + newUrl += '?' + query; + } + window.history.pushState({path: newUrl}, '', newUrl); } diff --git a/assets/sass/pages/_contributing.scss b/assets/sass/pages/_contributing.scss index 0cb97aa957..9b0f285b3c 100644 --- a/assets/sass/pages/_contributing.scss +++ b/assets/sass/pages/_contributing.scss @@ -26,6 +26,21 @@ } } + .issue-controls { + display: flex; + gap: 2em; + flex-wrap: wrap; + align-items: center; + + p { + margin: 0.5em 0; + } + + select { + margin-left: 0.5em; + } + } + ul.issues-list { li { .issue-label { diff --git a/contributing/open_issues.html b/contributing/open_issues.html index 46ff2ef020..48b6e178c2 100644 --- a/contributing/open_issues.html +++ b/contributing/open_issues.html @@ -17,18 +17,28 @@ {% endfor %} {% endfor %} {% endfor %} -

- Filter by issue labels - -

+
+

+ Filter by issue labels + +

+

+ Sort by + +

+

Open Issues