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
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 >
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 "> ×</ 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