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