Adrià Vilanova MartÃnez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 1 | // Copyright 2016 The Chromium Authors |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | /** |
| 6 | * This file contains JS functions used in rendering a hotlistissues table |
| 7 | */ |
| 8 | |
| 9 | |
| 10 | /** |
| 11 | * Helper function to set several attributes of an element at once. |
| 12 | * @param {Element} el element that is getting the attributes |
| 13 | * @param {dict} attrs Dictionary of {attrName: attrValue, ..} |
| 14 | */ |
| 15 | function setAttributes(el, attrs) { |
| 16 | for (let key in attrs) { |
| 17 | el.setAttribute(key, attrs[key]); |
| 18 | } |
| 19 | } |
| 20 | |
| 21 | // TODO(jojwang): readOnly is currently empty string, figure out what it should be |
| 22 | // ('True'/'False' 'yes'/'no'?). |
| 23 | |
| 24 | /** |
| 25 | * Helper function for creating a <td> element that contains the widgets of the row. |
| 26 | * @param {dict} tableRow dictionary {'projectName': 'name', .. } of relevant row info. |
| 27 | * @param {} readOnly. |
| 28 | * @param {boolean} userLoggedIn is the current user logged in. |
| 29 | * @return an element containing the widget elements |
| 30 | */ |
| 31 | function createWidgets(tableRow, readOnly, userLoggedIn) { |
| 32 | let widgets = document.createElement('td'); |
| 33 | widgets.setAttribute('class', 'rowwidgets nowrap'); |
| 34 | |
| 35 | let gripper = document.createElement('i'); |
| 36 | gripper.setAttribute('class', 'material-icons gripper'); |
| 37 | gripper.setAttribute('title', 'Drag issue'); |
| 38 | gripper.textContent = 'drag_indicator'; |
| 39 | widgets.appendChild(gripper); |
| 40 | |
| 41 | if (!readOnly) { |
| 42 | if (userLoggedIn) { |
| 43 | // TODO(jojwang): for bulk edit, only show a checkbox next to an issue that |
| 44 | // the user has permission to edit. |
| 45 | let checkbox = document.createElement('input'); |
| 46 | setAttributes(checkbox, {'class': 'checkRangeSelect', |
| 47 | 'id': 'cb_' + tableRow['issueRef'], |
| 48 | 'type': 'checkbox'}); |
| 49 | widgets.appendChild(checkbox); |
| 50 | widgets.appendChild(document.createTextNode(' ')); |
| 51 | |
| 52 | let star = document.createElement('a'); |
| 53 | let starColor = tableRow['isStarred'] ? 'cornflowerblue' : 'gray'; |
| 54 | let starred = tableRow['isStarred'] ? 'Un-s' : 'S'; |
| 55 | setAttributes(star, {'class': 'star', |
| 56 | 'id': 'star-' + tableRow['projectName'] + tableRow['localID'], |
| 57 | 'style': 'color:' + starColor, |
| 58 | 'title': starred + 'tar this issue', |
| 59 | 'data-project-name': tableRow['projectName'], |
| 60 | 'data-local-id': tableRow['localID']}); |
| 61 | star.textContent = (tableRow['isStarred'] ? '\u2605' : '\u2606'); |
| 62 | widgets.appendChild(star); |
| 63 | } |
| 64 | } |
| 65 | return widgets; |
| 66 | } |
| 67 | |
| 68 | |
| 69 | /** |
| 70 | * Helper function to set attributes and add Nodes for an ID cell. |
| 71 | * @param {Element} td element to be added to current row in table. |
| 72 | * @param {dict} tableRow dictionary {'projectName': 'name', .. } of relevant row info. |
| 73 | * @param {boolean} isCrossProject are issues in the table from more than one project. |
| 74 | */ |
| 75 | function createIDCell(td, tableRow, isCrossProject) { |
| 76 | td.classList.add('id'); |
| 77 | let aLink = document.createElement('a'); |
| 78 | aLink.setAttribute('href', tableRow['issueCleanURL']); |
| 79 | aLink.setAttribute('class', 'computehref'); |
| 80 | let aLinkContent = (isCrossProject ? (tableRow['projectName'] + ':') : '' ) + tableRow['localID']; |
| 81 | aLink.textContent = aLinkContent; |
| 82 | td.appendChild(aLink); |
| 83 | } |
| 84 | |
| 85 | function createProjectCell(td, tableRow) { |
| 86 | td.classList.add('project'); |
| 87 | let aLink = document.createElement('a'); |
| 88 | aLink.setAttribute('href', tableRow['projectURL']); |
| 89 | aLink.textContent = tableRow['projectName']; |
| 90 | td.appendChild(aLink); |
| 91 | } |
| 92 | |
| 93 | function createEditableNoteCell(td, cell, projectName, localID, hotlistID) { |
| 94 | let textBox = document.createElement('textarea'); |
| 95 | setAttributes(textBox, { |
| 96 | 'id': `itemnote_${projectName}_${localID}`, |
| 97 | 'placeholder': '---', |
| 98 | 'class': 'itemnote rowwidgets', |
| 99 | 'projectname': projectName, |
| 100 | 'localid': localID, |
| 101 | 'style': 'height:15px', |
| 102 | }); |
| 103 | if (cell['values'].length > 0) { |
| 104 | textBox.value = cell['values'][0]['item']; |
| 105 | } |
| 106 | textBox.addEventListener('blur', function(e) { |
| 107 | saveNote(e.target, hotlistID); |
| 108 | }); |
| 109 | debouncedKeyHandler = debounce(function(e) { |
| 110 | saveNote(e.target, hotlistID); |
| 111 | }); |
| 112 | textBox.addEventListener('keyup', debouncedKeyHandler, false); |
| 113 | td.appendChild(textBox); |
| 114 | } |
| 115 | |
| 116 | function enter_detector(e) { |
| 117 | if (e.which==13||e.keyCode==13) { |
| 118 | this.blur(); |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | |
| 123 | /** |
| 124 | * Helper function to set attributes and add Nodes for an Summary cell. |
| 125 | * @param {Element} td element to be added to current row in table. |
| 126 | * @param {dict} cell dictionary {'values': [], .. } of relevant cell info. |
| 127 | * @param {string=} projectName The name of the project the summary references. |
| 128 | */ |
| 129 | function createSummaryCell(td, cell, projectName) { |
| 130 | // TODO(jojwang): detect when links are present and make clicking on cell go |
| 131 | // to link, not issue details page |
| 132 | td.setAttribute('style', 'width:100%'); |
| 133 | fillValues(td, cell['values']); |
| 134 | fillNonColumnLabels(td, cell['nonColLabels'], projectName); |
| 135 | } |
| 136 | |
| 137 | |
| 138 | /** |
| 139 | * Helper function to set attributes and add Nodes for an Attribute or Unfilterable cell. |
| 140 | * @param {Element} td element to be added to current row in table. |
| 141 | * @param {dict} cell dictionary {'type': 'Summary', .. } of relevant cell info. |
| 142 | */ |
| 143 | function createAttrAndUnfiltCell(td, cell) { |
| 144 | if (cell['noWrap'] == 'yes') { |
| 145 | td.className += ' nowrapspan'; |
| 146 | } |
| 147 | if (cell['align']) { |
| 148 | td.setAttribute('align', cell['align']); |
| 149 | } |
| 150 | fillValues(td, cell['values']); |
| 151 | } |
| 152 | |
| 153 | function createUrlCell(td, cell) { |
| 154 | td.classList.add('url'); |
| 155 | cell.values.forEach((value) => { |
| 156 | let aLink = document.createElement('a'); |
| 157 | aLink.href = value['item']; |
| 158 | aLink.target = '_blank'; |
| 159 | aLink.rel = 'nofollow'; |
| 160 | aLink.textContent = value['item']; |
| 161 | aLink.classList.add('fieldvalue_url'); |
| 162 | td.appendChild(aLink); |
| 163 | }); |
| 164 | } |
| 165 | |
| 166 | function createIssuesCell(td, cell) { |
| 167 | td.classList.add('url'); |
| 168 | if (cell.values.length > 0) { |
| 169 | cell.values.forEach( function(value, index, array) { |
| 170 | const span = document.createElement('span'); |
| 171 | if (value['isDerived']) { |
| 172 | span.className = 'derived'; |
| 173 | } |
| 174 | const a = document.createElement('a'); |
| 175 | a.href = value['href']; |
| 176 | a.rel = 'nofollow"'; |
| 177 | if (value['title']) { |
| 178 | a.title = value['title']; |
| 179 | } |
| 180 | if (value['closed']) { |
| 181 | a.style.textDecoration = 'line-through'; |
| 182 | } |
| 183 | a.textContent = value['id']; |
| 184 | span.appendChild(a); |
| 185 | td.appendChild(span); |
| 186 | if (index != array.length-1) { |
| 187 | td.appendChild(document.createTextNode(', ')); |
| 188 | } |
| 189 | }); |
| 190 | } else { |
| 191 | td.textContent = '---'; |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * Helper function to fill a td element with a cell's non-column labels. |
| 197 | * @param {Element} td element to be added to current row in table. |
| 198 | * @param {list} labels list of dictionaries with relevant (key, value) for |
| 199 | * each label |
| 200 | * @param {string=} projectName The name of the project the labels reference. |
| 201 | */ |
| 202 | function fillNonColumnLabels(td, labels, projectName) { |
| 203 | labels.forEach( function(label) { |
| 204 | const aLabel = document.createElement('a'); |
| 205 | setAttributes(aLabel, |
| 206 | { |
| 207 | 'class': 'label', |
| 208 | 'href': `/p/${projectName}/issues/list?q=label:${label['value']}`, |
| 209 | }); |
| 210 | if (label['isDerived']) { |
| 211 | const i = document.createElement('i'); |
| 212 | i.textContent = label['value']; |
| 213 | aLabel.appendChild(i); |
| 214 | } else { |
| 215 | aLabel.textContent = label['value']; |
| 216 | } |
| 217 | td.appendChild(document.createTextNode(' ')); |
| 218 | td.appendChild(aLabel); |
| 219 | }); |
| 220 | } |
| 221 | |
| 222 | |
| 223 | /** |
| 224 | * Helper function to fill a td element with a cell's value(s). |
| 225 | * @param {Element} td element to be added to current row in table. |
| 226 | * @param {list} values list of dictionaries with relevant (key, value) for each value |
| 227 | */ |
| 228 | function fillValues(td, values) { |
| 229 | if (values.length > 0) { |
| 230 | values.forEach( function(value, index, array) { |
| 231 | let span = document.createElement('span'); |
| 232 | if (value['isDerived']) { |
| 233 | span.className = 'derived'; |
| 234 | } |
| 235 | span.textContent = value['item']; |
| 236 | td.appendChild(span); |
| 237 | if (index != array.length-1) { |
| 238 | td.appendChild(document.createTextNode(', ')); |
| 239 | } |
| 240 | }); |
| 241 | } else { |
| 242 | td.textContent = '---'; |
| 243 | } |
| 244 | } |
| 245 | |
| 246 | |
| 247 | /** |
| 248 | * Helper function to create a table row. |
| 249 | * @param {dict} tableRow dictionary {'projectName': 'name', .. } of relevant row info. |
| 250 | * @param {dict} pageSettings dict of relevant settings for the hotlist and user viewing the page. |
| 251 | */ |
| 252 | function renderHotlistRow(tableRow, pageSettings) { |
| 253 | let tr = document.createElement('tr'); |
| 254 | if (pageSettings['cursor'] == tableRow['issueRef']) { |
| 255 | tr.setAttribute('class', 'ifOpened hoverTarget cursor_on drag_item'); |
| 256 | } else { |
| 257 | tr.setAttribute('class', 'ifOpened hoverTarget cursor_off drag_item'); |
| 258 | } |
| 259 | |
| 260 | setAttributes(tr, {'data-idx': tableRow['idx'], 'data-id': tableRow['issueID'], 'issue-context-url': tableRow['issueContextURL']}); |
| 261 | widgets = createWidgets(tableRow, pageSettings['readOnly'], |
| 262 | pageSettings['userLoggedIn']); |
| 263 | tr.appendChild(widgets); |
| 264 | tableRow['cells'].forEach(function(cell) { |
| 265 | let td = document.createElement('td'); |
| 266 | td.setAttribute('class', 'col_' + cell['colIndex']); |
| 267 | if (cell['type'] == 'ID') { |
| 268 | createIDCell(td, tableRow, (pageSettings['isCrossProject'] == 'True')); |
| 269 | } else if (cell['type'] == 'summary') { |
| 270 | createSummaryCell(td, cell, tableRow['projectName']); |
| 271 | } else if (cell['type'] == 'note') { |
| 272 | if (pageSettings['ownerPerm'] || pageSettings['editorPerm']) { |
| 273 | createEditableNoteCell( |
| 274 | td, cell, tableRow['projectName'], tableRow['localID'], |
| 275 | pageSettings['hotlistID']); |
| 276 | } else { |
| 277 | createSummaryCell(td, cell, tableRow['projectName']); |
| 278 | } |
| 279 | } else if (cell['type'] == 'project') { |
| 280 | createProjectCell(td, tableRow); |
| 281 | } else if (cell['type'] == 'url') { |
| 282 | createUrlCell(td, cell); |
| 283 | } else if (cell['type'] == 'issues') { |
| 284 | createIssuesCell(td, cell); |
| 285 | } else { |
| 286 | createAttrAndUnfiltCell(td, cell); |
| 287 | } |
| 288 | tr.appendChild(td); |
| 289 | }); |
| 290 | let directLinkURL = tableRow['issueCleanURL']; |
| 291 | let directLink = document.createElement('a'); |
| 292 | directLink.setAttribute('class', 'directlink material-icons'); |
| 293 | directLink.setAttribute('href', directLinkURL); |
| 294 | directLink.textContent = 'link'; // Renders as a link icon. |
| 295 | let lastCol = document.createElement('td'); |
| 296 | lastCol.appendChild(directLink); |
| 297 | tr.appendChild(lastCol); |
| 298 | return tr; |
| 299 | } |
| 300 | |
| 301 | |
| 302 | /** |
| 303 | * Helper function to create the group header row |
| 304 | * @param {dict} group dict of relevant values for the current group |
| 305 | * @return a <tr> element to be added to the current <tbody> |
| 306 | */ |
| 307 | function renderGroupRow(group) { |
| 308 | let tr = document.createElement('tr'); |
| 309 | tr.setAttribute('class', 'group_row'); |
| 310 | let td = document.createElement('td'); |
| 311 | setAttributes(td, {'colspan': '100', 'class': 'toggleHidden'}); |
| 312 | let whenClosedImg = document.createElement('img'); |
| 313 | setAttributes(whenClosedImg, {'class': 'ifClosed', 'src': '/static/images/plus.gif'}); |
| 314 | td.appendChild(whenClosedImg); |
| 315 | let whenOpenImg = document.createElement('img'); |
| 316 | setAttributes(whenOpenImg, {'class': 'ifOpened', 'src': '/static/images/minus.gif'}); |
| 317 | td.appendChild(whenOpenImg); |
| 318 | tr.appendChild(td); |
| 319 | |
| 320 | div = document.createElement('div'); |
| 321 | div.textContent += group['rowsInGroup']; |
| 322 | |
| 323 | div.textContent += (group['rowsInGroup'] == '1' ? ' issue:': ' issues:'); |
| 324 | |
| 325 | group['cells'].forEach(function(cell) { |
| 326 | let hasValue = false; |
| 327 | cell['values'].forEach(function(value) { |
| 328 | if (value['item'] !== 'None') { |
| 329 | hasValue = true; |
| 330 | } |
| 331 | }); |
| 332 | if (hasValue) { |
| 333 | cell.values.forEach(function(value) { |
| 334 | div.textContent += (' ' + cell['groupName'] + '=' + value['item']); |
| 335 | }); |
| 336 | } else { |
| 337 | div.textContent += (' -has:' + cell['groupName']); |
| 338 | } |
| 339 | }); |
| 340 | td.appendChild(div); |
| 341 | return tr; |
| 342 | } |
| 343 | |
| 344 | |
| 345 | /** |
| 346 | * Builds the body of a hotlistissues table. |
| 347 | * @param {dict} tableData dict of relevant values from 'table_data' |
| 348 | * @param {dict} pageSettings dict of relevant settings for the hotlist and user viewing the page. |
| 349 | */ |
| 350 | function renderHotlistTable(tableData, pageSettings) { |
| 351 | let tbody; |
| 352 | let table = $('resultstable'); |
| 353 | |
| 354 | // TODO(jojwang): this would not work if grouping did not require a page refresh |
| 355 | // that wiped the table of all its children. This should be redone to be more |
| 356 | // robust. |
| 357 | // This loop only does anything when reranking is enabled. |
| 358 | for (i=0; i < table.childNodes.length; i++) { |
| 359 | if (table.childNodes[i].tagName == 'TBODY') { |
| 360 | table.removeChild(table.childNodes[i]); |
| 361 | } |
| 362 | } |
| 363 | |
| 364 | tableData.forEach(function(tableRow) { |
| 365 | if (tableRow['group'] !== 'no') { |
| 366 | // add current tbody to table, need a new tbody with group row |
| 367 | if (typeof tbody !== 'undefined') { |
| 368 | table.appendChild(tbody); |
| 369 | } |
| 370 | tbody = document.createElement('tbody'); |
| 371 | tbody.setAttribute('class', 'opened'); |
| 372 | tbody.appendChild(renderGroupRow(tableRow['group'])); |
| 373 | } |
| 374 | if (typeof tbody == 'undefined') { |
| 375 | tbody = document.createElement('tbody'); |
| 376 | } |
| 377 | tbody.appendChild(renderHotlistRow(tableRow, pageSettings)); |
| 378 | }); |
| 379 | tbody.appendChild(document.createElement('tr')); |
| 380 | table.appendChild(tbody); |
| 381 | |
| 382 | let stars = document.getElementsByClassName('star'); |
| 383 | for (var i = 0; i < stars.length; ++i) { |
| 384 | let star = stars[i]; |
| 385 | star.addEventListener('click', function(event) { |
| 386 | let projectName = event.target.getAttribute('data-project-name'); |
| 387 | let localID = event.target.getAttribute('data-local-id'); |
| 388 | _TKR_toggleStar(event.target, projectName, localID, null, null, null); |
| 389 | }); |
| 390 | } |
| 391 | } |
| 392 | |
| 393 | |
| 394 | /** |
| 395 | * Activates the drag and drop functionality of the hotlistissues table. |
| 396 | * @param {dict} tableData dict of relevant values from the 'table_data' of |
| 397 | * hotlistissues servlet. This is used when a drag and drop motion does not |
| 398 | * result in any changes in the ordering of the issues. |
| 399 | * @param {dict} pageSettings dict of relevant settings for the hotlist and user |
| 400 | * viewing the page. |
| 401 | * @param {str} hotlistID the number ID of the current hotlist |
| 402 | */ |
| 403 | function activateDragDrop(tableData, pageSettings, hotlistID) { |
| 404 | function onHotlistRerank(srcID, targetID, position) { |
| 405 | let data = { |
| 406 | target_id: targetID, |
| 407 | moved_ids: srcID, |
| 408 | split_above: position == 'above', |
| 409 | colspec: pageSettings['colSpec'], |
| 410 | can: pageSettings['can'], |
| 411 | }; |
| 412 | CS_doPost(hotlistID + '/rerank.do', onHotlistResponse, data); |
| 413 | } |
| 414 | |
| 415 | function onHotlistResponse(event) { |
| 416 | let xhr = event.target; |
| 417 | if (xhr.readyState != 4) { |
| 418 | return; |
| 419 | } |
| 420 | if (xhr.status != 200) { |
| 421 | window.console.error('200 page error'); |
| 422 | // TODO(jojwang): fill this in more |
| 423 | return; |
| 424 | } |
| 425 | let response = CS_parseJSON(xhr); |
| 426 | renderHotlistTable( |
| 427 | (response['table_data'] == '' ? tableData : response['table_data']), |
| 428 | pageSettings); |
| 429 | // TODO(jojwang): pass pagination state to server |
| 430 | _initDragAndDrop($('resultstable'), onHotlistRerank, true); |
| 431 | } |
| 432 | _initDragAndDrop($('resultstable'), onHotlistRerank, true); |
| 433 | } |