Skip to content

Commit 90f2612

Browse files
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
1 parent 17c2dcc commit 90f2612

File tree

3 files changed

+212
-28
lines changed

3 files changed

+212
-28
lines changed

assets/javascript/contributing.js

Lines changed: 175 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
document.addEventListener('DOMContentLoaded', function() {
22
// only load on open issues page for now
3-
var issueSelect = document.querySelector(".open-issues select");
3+
var issueSelect = document.querySelector(".open-issues #label-filter");
44
if (!issueSelect) {
55
return;
66
}
77

8-
issueSelect.onchange = issueSelectHandler;
8+
issueSelect.onchange = function(event) {
9+
issueSelectHandler(event);
10+
applySorting();
11+
};
12+
13+
var sortSelect = document.querySelector(".open-issues #sort-order");
14+
if (sortSelect) {
15+
sortSelect.onchange = function() {
16+
applySorting();
17+
};
18+
}
919

1020
// load issues label when using back button
1121
window.addEventListener('popstate', loadIssues.bind(null, true));
@@ -17,27 +27,35 @@ document.addEventListener('DOMContentLoaded', function() {
1727
function loadIssues(isPopState) {
1828
var params = new URLSearchParams(window.location.search);
1929
var label = params.get('label');
30+
var sort = params.get('sort');
2031

21-
if (!label) {
22-
return;
32+
if (sort) {
33+
var sortSelect = document.querySelector('.open-issues #sort-order');
34+
if (sortSelect) {
35+
sortSelect.value = sort;
36+
}
37+
}
38+
39+
if (label) {
40+
issueSelectHandler(label, isPopState);
41+
var issuesList = document.querySelector('.open-issues #label-filter');
42+
issuesList.value = label;
2343
}
2444

25-
issueSelectHandler(label, isPopState);
26-
var issuesList = document.querySelector('.open-issues select');
27-
issuesList.value = label;
45+
applySorting();
2846
}
2947

3048
function issueSelectHandler(event, isPopState) {
3149
if (event.target) {
32-
var selectedOption = this.options[this.selectedIndex].value;
50+
var selectedOption = event.target.options[event.target.selectedIndex].value;
3351
} else {
3452
// page loads will set the event as just the selected label from params
3553
var selectedOption = event;
3654
}
3755

3856
// don't set params on the back button
3957
if (!isPopState) {
40-
setIssueParams(selectedOption);
58+
setParams();
4159
}
4260

4361
// hide all elements first
@@ -47,17 +65,158 @@ function issueSelectHandler(event, isPopState) {
4765
});
4866

4967
// show the selected options
50-
var selectedOption = selectedOption === 'all' ? 'li' : `.${selectedOption}`;
51-
var items = document.querySelectorAll(`.issues-list ${selectedOption}`);
68+
var selector = selectedOption === 'all' ? 'li' : '.' + selectedOption;
69+
var items = document.querySelectorAll('.issues-list ' + selector);
5270
items.forEach(function(item) {
53-
item.style.display = 'block'
71+
item.style.display = 'block';
5472
item.parentElement.closest('li').style.display = 'block';
5573
});
5674
}
5775

58-
function setIssueParams(label) {
76+
function getIssueDays(element) {
77+
// Parse "(Open X days)" from the issue title text
78+
var text = element.textContent || '';
79+
var match = text.match(/\(Open\s+(\d+)\s+days?\)/i);
80+
if (match) {
81+
return parseInt(match[1], 10);
82+
}
83+
return 0;
84+
}
85+
86+
function applySorting() {
87+
var sortSelect = document.querySelector('.open-issues #sort-order');
88+
if (!sortSelect) return;
89+
90+
var sortOrder = sortSelect.value;
91+
if (sortOrder === 'default') {
92+
// Restore original order by reloading — but simpler to just not sort
93+
// We store original order on first run
94+
restoreOriginalOrder();
95+
setParams();
96+
return;
97+
}
98+
99+
// Sort issues within each library's issues-list
100+
var issuesLists = document.querySelectorAll('.issues-list');
101+
issuesLists.forEach(function(list) {
102+
var items = Array.from(list.querySelectorAll(':scope > li'));
103+
104+
// Store original order if not already stored
105+
if (!list.dataset.originalOrder) {
106+
list.dataset.originalOrder = 'stored';
107+
items.forEach(function(item, index) {
108+
item.dataset.originalIndex = index;
109+
});
110+
}
111+
112+
items.sort(function(a, b) {
113+
var daysA = getIssueDays(a);
114+
var daysB = getIssueDays(b);
115+
if (sortOrder === 'newest') {
116+
return daysA - daysB; // fewer days = newer = first
117+
} else {
118+
return daysB - daysA; // more days = older = first
119+
}
120+
});
121+
122+
// Re-append in sorted order
123+
items.forEach(function(item) {
124+
list.appendChild(item);
125+
});
126+
});
127+
128+
// Also sort the library-level list items by their oldest/newest issue
129+
var topList = document.querySelector('.open-issues > ul');
130+
if (topList) {
131+
var libraryItems = Array.from(topList.querySelectorAll(':scope > li'));
132+
133+
if (!topList.dataset.originalOrder) {
134+
topList.dataset.originalOrder = 'stored';
135+
libraryItems.forEach(function(item, index) {
136+
item.dataset.originalIndex = index;
137+
});
138+
}
139+
140+
libraryItems.sort(function(a, b) {
141+
var issuesA = a.querySelectorAll('.issues-list > li');
142+
var issuesB = b.querySelectorAll('.issues-list > li');
143+
var maxA = getMaxDays(issuesA, sortOrder);
144+
var maxB = getMaxDays(issuesB, sortOrder);
145+
if (sortOrder === 'newest') {
146+
return maxA - maxB;
147+
} else {
148+
return maxB - maxA;
149+
}
150+
});
151+
152+
libraryItems.forEach(function(item) {
153+
topList.appendChild(item);
154+
});
155+
}
156+
157+
setParams();
158+
}
159+
160+
function getMaxDays(issues, sortOrder) {
161+
var result = sortOrder === 'newest' ? Infinity : 0;
162+
issues.forEach(function(issue) {
163+
var days = getIssueDays(issue);
164+
if (sortOrder === 'newest') {
165+
result = Math.min(result, days);
166+
} else {
167+
result = Math.max(result, days);
168+
}
169+
});
170+
return result === Infinity ? 0 : result;
171+
}
172+
173+
function restoreOriginalOrder() {
174+
// Restore library-level order
175+
var topList = document.querySelector('.open-issues > ul');
176+
if (topList && topList.dataset.originalOrder) {
177+
var libraryItems = Array.from(topList.querySelectorAll(':scope > li'));
178+
libraryItems.sort(function(a, b) {
179+
return (parseInt(a.dataset.originalIndex) || 0) - (parseInt(b.dataset.originalIndex) || 0);
180+
});
181+
libraryItems.forEach(function(item) {
182+
topList.appendChild(item);
183+
});
184+
}
185+
186+
// Restore issue-level order within each list
187+
var issuesLists = document.querySelectorAll('.issues-list');
188+
issuesLists.forEach(function(list) {
189+
var items = Array.from(list.querySelectorAll(':scope > li'));
190+
items.sort(function(a, b) {
191+
return (parseInt(a.dataset.originalIndex) || 0) - (parseInt(b.dataset.originalIndex) || 0);
192+
});
193+
items.forEach(function(item) {
194+
list.appendChild(item);
195+
});
196+
});
197+
}
198+
199+
function setParams() {
59200
var params = new URLSearchParams(window.location.search);
60-
params.set("label", label);
61-
var newUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}?${params.toString()}`;
62-
window.history.pushState({path:newUrl}, '', newUrl);
201+
202+
var labelSelect = document.querySelector('.open-issues #label-filter');
203+
if (labelSelect && labelSelect.value && labelSelect.value !== 'all') {
204+
params.set("label", labelSelect.value);
205+
} else {
206+
params.delete("label");
207+
}
208+
209+
var sortSelect = document.querySelector('.open-issues #sort-order');
210+
if (sortSelect && sortSelect.value && sortSelect.value !== 'default') {
211+
params.set("sort", sortSelect.value);
212+
} else {
213+
params.delete("sort");
214+
}
215+
216+
var query = params.toString();
217+
var newUrl = window.location.protocol + '//' + window.location.host + window.location.pathname;
218+
if (query) {
219+
newUrl += '?' + query;
220+
}
221+
window.history.pushState({path: newUrl}, '', newUrl);
63222
}

assets/sass/pages/_contributing.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,21 @@
2626
}
2727
}
2828

29+
.issue-controls {
30+
display: flex;
31+
gap: 2em;
32+
flex-wrap: wrap;
33+
align-items: center;
34+
35+
p {
36+
margin: 0.5em 0;
37+
}
38+
39+
select {
40+
margin-left: 0.5em;
41+
}
42+
}
43+
2944
ul.issues-list {
3045
li {
3146
.issue-label {

contributing/open_issues.html

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,28 @@
1717
{% endfor %}
1818
{% endfor %}
1919
{% endfor %}
20-
<p>
21-
Filter by issue labels
22-
<select>
23-
{% for label in labels %}
24-
{% assign lowerlabel = label | downcase %}
25-
{% assign ddlabels = ddlabels | push: lowerlabel | uniq %}
26-
{% endfor %}
27-
{% for label in ddlabels %}
28-
<option value='{{ label | replace: ' ', '-' }}'>{{ label | capitalize }}</option>
29-
{% endfor %}
30-
<select>
31-
</p>
20+
<div class="issue-controls">
21+
<p>
22+
Filter by issue labels
23+
<select id="label-filter">
24+
{% for label in labels %}
25+
{% assign lowerlabel = label | downcase %}
26+
{% assign ddlabels = ddlabels | push: lowerlabel | uniq %}
27+
{% endfor %}
28+
{% for label in ddlabels %}
29+
<option value='{{ label | replace: ' ', '-' }}'>{{ label | capitalize }}</option>
30+
{% endfor %}
31+
</select>
32+
</p>
33+
<p>
34+
Sort by
35+
<select id="sort-order">
36+
<option value="default">Default</option>
37+
<option value="newest">Newest first</option>
38+
<option value="oldest">Oldest first</option>
39+
</select>
40+
</p>
41+
</div>
3242
<h3>Open Issues</h3>
3343
<ul>
3444
{% for library in site.data.libraries.open_issues %}

0 commit comments

Comments
 (0)