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
+
+ {% for label in labels %}
+ {% assign lowerlabel = label | downcase %}
+ {% assign ddlabels = ddlabels | push: lowerlabel | uniq %}
+ {% endfor %}
+ {% for label in ddlabels %}
+
+ {% endfor %}
+
+
+
+ Sort by
+
+
+
+
+
+
+
Open Issues
{% for library in site.data.libraries.open_issues %}
From 8ccb280756efeb1adda7dbf62d95ca72330697a2 Mon Sep 17 00:00:00 2001
From: Piclaw
Date: Wed, 15 Apr 2026 10:55:29 -0700
Subject: [PATCH 2/3] Use data-days-open attribute instead of parsing title
text
Simplify sorting by reading days_open from the issue data via a
data attribute on each li element, replacing the regex title parser.
---
assets/javascript/contributing.js | 62 +------------------------------
contributing/open_issues.html | 2 +-
2 files changed, 2 insertions(+), 62 deletions(-)
diff --git a/assets/javascript/contributing.js b/assets/javascript/contributing.js
index f66929ca32..1980de7c15 100644
--- a/assets/javascript/contributing.js
+++ b/assets/javascript/contributing.js
@@ -74,13 +74,7 @@ function issueSelectHandler(event, isPopState) {
}
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;
+ return parseInt(element.dataset.daysOpen, 10) || 0;
}
function applySorting() {
@@ -125,64 +119,10 @@ function applySorting() {
});
});
- // 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) {
diff --git a/contributing/open_issues.html b/contributing/open_issues.html
index 48b6e178c2..87d3347641 100644
--- a/contributing/open_issues.html
+++ b/contributing/open_issues.html
@@ -51,7 +51,7 @@ Open Issues
{{ label | downcase | replace: ' ', '-' }}
{% endfor %}
{% endcapture %}
- - {{ issue.title }}
+
- {{ issue.title }}
{% for label in issue.labels %}
{% if label != 'None' %}
{{label}}
From b3c9de4d5b7797465f18a08aa6f12527316179e3 Mon Sep 17 00:00:00 2001
From: Piclaw
Date: Wed, 15 Apr 2026 12:50:38 -0700
Subject: [PATCH 3/3] Add library group sorting by issue age
Sort library groups alongside issues when using Newest/Oldest sort.
Libraries are ordered by their best matching issue (min days for
newest, max days for oldest). Default restores alphabetical order.
- Add id='libraries-list' to top ul for reliable targeting
- Add getBestDays() helper to find representative issue age per library
- Restore both library and issue order when switching back to Default
---
assets/javascript/contributing.js | 52 +++++++++++++++++++++++++++++++
contributing/open_issues.html | 2 +-
2 files changed, 53 insertions(+), 1 deletion(-)
diff --git a/assets/javascript/contributing.js b/assets/javascript/contributing.js
index 1980de7c15..7d60151459 100644
--- a/assets/javascript/contributing.js
+++ b/assets/javascript/contributing.js
@@ -119,10 +119,62 @@ function applySorting() {
});
});
+ // Sort the library groups by their best matching issue
+ var topList = document.getElementById('libraries-list');
+ 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 bestA = getBestDays(a.querySelectorAll('.issues-list > li'), sortOrder);
+ var bestB = getBestDays(b.querySelectorAll('.issues-list > li'), sortOrder);
+ if (sortOrder === 'newest') {
+ return bestA - bestB;
+ } else {
+ return bestB - bestA;
+ }
+ });
+
+ libraryItems.forEach(function(item) {
+ topList.appendChild(item);
+ });
+ }
+
setParams();
}
+function getBestDays(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.getElementById('libraries-list');
+ 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) {
diff --git a/contributing/open_issues.html b/contributing/open_issues.html
index 87d3347641..13d8f698a6 100644
--- a/contributing/open_issues.html
+++ b/contributing/open_issues.html
@@ -40,7 +40,7 @@
Open Issues
-
+
{% for library in site.data.libraries.open_issues %}
-
{{library[0]}}