Skip to content

Commit a007de5

Browse files
authored
Merge pull request #451 from Sakib25800/queue-select-column
Add rollup selection column UI
2 parents 0d5b4b7 + 37565cd commit a007de5

File tree

2 files changed

+186
-28
lines changed

2 files changed

+186
-28
lines changed

src/database/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,12 @@ impl PullRequestModel {
416416
},
417417
}
418418
}
419+
420+
/// Determines if this PR can be included in a rollup.
421+
/// A PR is rollupable if it has been approved and rollup is not `RollupMode::Never`
422+
pub fn is_rollupable(&self) -> bool {
423+
self.is_approved() && !matches!(self.rollup, Some(RollupMode::Never))
424+
}
419425
}
420426

421427
/// Describes whether a workflow is a Github Actions workflow or if it's a job from some external

templates/queue.html

Lines changed: 180 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,35 @@
2626
table td {
2727
padding: 0.5rem;
2828
}
29+
30+
th.select-checkbox,
31+
td.select-checkbox {
32+
width: 2.5rem;
33+
}
34+
35+
#rollupModal {
36+
display: none;
37+
position: fixed;
38+
z-index: 1000;
39+
left: 0;
40+
top: 0;
41+
width: 100%;
42+
height: 100%;
43+
background-color: rgba(0,0,0,0.5);
44+
}
45+
46+
#rollupModalContent {
47+
background-color: white;
48+
margin: 15% auto;
49+
padding: 1rem;
50+
border: 1px solid black;
51+
max-width: 500px;
52+
}
53+
54+
#rollupModalClose {
55+
float: right;
56+
cursor: pointer;
57+
}
2958
</style>
3059
{% endblock %}
3160

@@ -53,17 +82,19 @@ <h1>
5382
<label for="groupBy">Group by: </label>
5483
<select id="groupBy">
5584
<option value="">None</option>
56-
<option value="1">Status</option>
57-
<option value="2">Mergeable</option>
58-
<option value="4">Author</option>
59-
<option value="7">Priority</option>
85+
<option value="status">Status</option>
86+
<option value="mergeable">Mergeable</option>
87+
<option value="author">Author</option>
88+
<option value="priority">Priority</option>
6089
</select>
90+
<button id="showRollupSelection" style="margin-left: 1rem;">Create rollup</button>
6191
</div>
6292

6393
<div class="table-wrapper">
6494
<table id="table">
6595
<thead>
6696
<tr>
97+
<th class="select-checkbox"></th>
6798
<th>#</th>
6899
<th>Status</th>
69100
<th>Mergeable</th>
@@ -78,7 +109,8 @@ <h1>
78109

79110
<tbody>
80111
{% for pr in prs %}
81-
<tr>
112+
<tr data-rollupable="{{ pr.is_rollupable() }}">
113+
<td class="select-checkbox"></td>
82114
<td>
83115
<a href="{{ repo_url }}/pull/{{ pr.number }}">{{ pr.number.0 }}</a>
84116
</td>
@@ -121,24 +153,50 @@ <h1>
121153
<div style="text-align: center; margin-top: 1em;">
122154
<a href="https://github.com/rust-lang/bors">Contribute on GitHub</a>
123155
</div>
156+
157+
<div id="rollupModal">
158+
<div id="rollupModalContent">
159+
<span id="rollupModalClose">&times;</span>
160+
<p id="rollupModalMessage"></p>
161+
<button id="rollupModalContinue" style="display: none;">Continue</button>
162+
</div>
163+
</div>
124164
</main>
125165

126166
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
127167
<script src="https://cdn.datatables.net/2.3.4/js/dataTables.min.js"></script>
128-
<script src="https://cdn.datatables.net/rowgroup/1.5.1/js/dataTables.rowGroup.min.js"></script>
168+
<script src="https://cdn.datatables.net/rowgroup/1.6.0/js/dataTables.rowGroup.min.js"></script>
169+
<script src="https://cdn.datatables.net/select/3.1.3/js/dataTables.select.min.js"></script>
129170

130171
<script>
131-
const getDataStatusFromCell = (cell) => cell?.dataset?.status || '';
172+
const getDataStatusFromCell = (cell) => cell?.dataset?.status || "";
173+
const interactiveSelector = "a, button, input, label, select, textarea";
132174

133-
function initializeTable(colIndex) {
175+
function initializeTable(groupByColumnName) {
134176
let config = {
135177
paging: false,
136178
info: false,
179+
columns: [
180+
{ name: "select", orderable: false, className: "select-checkbox" },
181+
{ name: "number" },
182+
{ name: "status" },
183+
{ name: "mergeable" },
184+
{ name: "title" },
185+
{ name: "author" },
186+
{ name: "assignees" },
187+
{ name: "approved_by" },
188+
{ name: "priority" },
189+
{ name: "rollup" }
190+
],
137191
columnDefs: [
138192
{
139-
targets: 1, // Column 1 (Status column)
140-
render: function(data, type, row, meta) {
141-
if (type === 'display') {
193+
targets: "select:name",
194+
render: DataTable.render.select()
195+
},
196+
{
197+
targets: "status:name",
198+
render: (data, type, row, meta) => {
199+
if (type === "display") {
142200
return data;
143201
}
144202

@@ -154,39 +212,133 @@ <h1>
154212
}
155213
}
156214
],
157-
order: []
215+
order: [],
216+
select: {
217+
style: "multi",
218+
selector: "td.select-checkbox",
219+
headerCheckbox: true,
220+
selectable: (rowData, tr, index) => {
221+
return tr && tr.dataset && tr.dataset.rollupable === "true";
222+
}
223+
}
158224
};
159225

160-
if (colIndex !== null) {
161-
config.order = [[colIndex, "asc"]];
226+
if (groupByColumnName) {
227+
config.order = [[groupByColumnName + ":name", "asc"]];
162228
config.rowGroup = {
163-
dataSrc: colIndex === 1
164-
? ([_, html]) => {
165-
let table = document.getElementById('table');
166-
if (table && table.tBodies[0]) {
167-
let rows = Array.from(table.tBodies[0].rows);
168-
for (let row of rows) {
169-
if (row.cells[1] && row.cells[1].innerHTML === html) {
170-
return getDataStatusFromCell(row.cells[1]);
171-
}
229+
dataSrc: groupByColumnName === "status"
230+
? (row, type) => {
231+
let table = $("#table").DataTable();
232+
let statusIndex = table.column("status:name").index();
233+
let statusHtml = row[statusIndex];
234+
235+
// Find the corresponding DOM cell to get data-status attribute
236+
let tableRows = document.querySelectorAll("#table tbody tr");
237+
for (let tableRow of tableRows) {
238+
let statusCell = tableRow.cells[statusIndex];
239+
if (statusCell && statusCell.innerHTML.trim() === statusHtml.trim()) {
240+
return getDataStatusFromCell(statusCell);
172241
}
173242
}
174-
return html;
243+
244+
// Fallback to HTML content
245+
return statusHtml;
246+
}
247+
: (row, type) => {
248+
let table = $("#table").DataTable();
249+
let colIndex = table.column(groupByColumnName + ":name").index();
250+
return row[colIndex];
175251
}
176-
: colIndex
177252
};
178253
}
179-
180254
return new DataTable("#table", config);
181255
}
182256

257+
function bindRowClick(tableInstance) {
258+
const tbody = document.querySelector("#table tbody");
259+
if (!tbody) {
260+
return () => {};
261+
}
262+
263+
const handler = (event) => {
264+
// Ignore clicks on checkbox - let checkbox handle it
265+
if (event.target.closest("td.select-checkbox")) {
266+
return;
267+
}
268+
269+
// Ignore clicks on interactive elements
270+
if (event.target.closest(interactiveSelector)) {
271+
return;
272+
}
273+
274+
const rowElement = event.target.closest("tr");
275+
if (!rowElement) {
276+
return;
277+
}
278+
279+
const rowApi = tableInstance.row(rowElement);
280+
if (!rowApi.any()) {
281+
return;
282+
}
283+
284+
if (rowApi.selected()) {
285+
rowApi.deselect();
286+
} else {
287+
rowApi.select();
288+
}
289+
};
290+
291+
tbody.addEventListener("click", handler);
292+
return () => tbody.removeEventListener("click", handler);
293+
}
294+
183295
let table = initializeTable(null);
296+
let detachRowClick = bindRowClick(table);
184297

185298
// Handle group by dropdown changes
186299
document.getElementById("groupBy").addEventListener("change", function() {
187-
let colIndex = this.value === "" ? null : parseInt(this.value);
300+
let groupByColumnName = this.value === "" ? null : this.value;
188301
table.destroy();
189-
table = initializeTable(colIndex);
302+
detachRowClick();
303+
table = initializeTable(groupByColumnName);
304+
detachRowClick = bindRowClick(table);
305+
});
306+
307+
const modal = document.getElementById("rollupModal");
308+
const modalMessage = document.getElementById("rollupModalMessage");
309+
const modalClose = document.getElementById("rollupModalClose");
310+
const modalContinue = document.getElementById("rollupModalContinue");
311+
312+
function closeModal() {
313+
modal.style.display = "none";
314+
modalContinue.style.display = "none";
315+
}
316+
317+
modalClose.addEventListener("click", closeModal);
318+
modalContinue.addEventListener("click", closeModal);
319+
320+
window.addEventListener("click", function(event) {
321+
if (event.target === modal) {
322+
closeModal();
323+
}
324+
});
325+
326+
document.getElementById("showRollupSelection").addEventListener("click", function() {
327+
let selectedRows = table.rows({ selected: true }).nodes().toArray();
328+
let message;
329+
330+
if (selectedRows.length === 0) {
331+
message = "No PRs selected for rollup.";
332+
modalContinue.style.display = "none";
333+
} else {
334+
message = `You've selected <strong>${selectedRows.length} PR(s)</strong> to be included in this rollup.<br><br>
335+
A rollup is useful for shortening the queue, but jumping the queue is unfair to older PRs who have waited too long.<br><br>
336+
When creating a real rollup, see the <a href="https://forge.rust-lang.org/release/rollups.html" target="_blank">instructions</a> for reference.`;
337+
modalContinue.style.display = "inline-block";
338+
}
339+
340+
modalMessage.innerHTML = message;
341+
modal.style.display = "block";
190342
});
191343
</script>
192344
{% endblock %}

0 commit comments

Comments
 (0)