| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * This file contains JS functions used in rendering a hotlistissues table |
| */ |
| |
| |
| /** |
| * Helper function to set several attributes of an element at once. |
| * @param {Element} el element that is getting the attributes |
| * @param {dict} attrs Dictionary of {attrName: attrValue, ..} |
| */ |
| function setAttributes(el, attrs) { |
| for (let key in attrs) { |
| el.setAttribute(key, attrs[key]); |
| } |
| } |
| |
| // TODO(jojwang): readOnly is currently empty string, figure out what it should be |
| // ('True'/'False' 'yes'/'no'?). |
| |
| /** |
| * Helper function for creating a <td> element that contains the widgets of the row. |
| * @param {dict} tableRow dictionary {'projectName': 'name', .. } of relevant row info. |
| * @param {} readOnly. |
| * @param {boolean} userLoggedIn is the current user logged in. |
| * @return an element containing the widget elements |
| */ |
| function createWidgets(tableRow, readOnly, userLoggedIn) { |
| let widgets = document.createElement('td'); |
| widgets.setAttribute('class', 'rowwidgets nowrap'); |
| |
| let gripper = document.createElement('i'); |
| gripper.setAttribute('class', 'material-icons gripper'); |
| gripper.setAttribute('title', 'Drag issue'); |
| gripper.textContent = 'drag_indicator'; |
| widgets.appendChild(gripper); |
| |
| if (!readOnly) { |
| if (userLoggedIn) { |
| // TODO(jojwang): for bulk edit, only show a checkbox next to an issue that |
| // the user has permission to edit. |
| let checkbox = document.createElement('input'); |
| setAttributes(checkbox, {'class': 'checkRangeSelect', |
| 'id': 'cb_' + tableRow['issueRef'], |
| 'type': 'checkbox'}); |
| widgets.appendChild(checkbox); |
| widgets.appendChild(document.createTextNode(' ')); |
| |
| let star = document.createElement('a'); |
| let starColor = tableRow['isStarred'] ? 'cornflowerblue' : 'gray'; |
| let starred = tableRow['isStarred'] ? 'Un-s' : 'S'; |
| setAttributes(star, {'class': 'star', |
| 'id': 'star-' + tableRow['projectName'] + tableRow['localID'], |
| 'style': 'color:' + starColor, |
| 'title': starred + 'tar this issue', |
| 'data-project-name': tableRow['projectName'], |
| 'data-local-id': tableRow['localID']}); |
| star.textContent = (tableRow['isStarred'] ? '\u2605' : '\u2606'); |
| widgets.appendChild(star); |
| } |
| } |
| return widgets; |
| } |
| |
| |
| /** |
| * Helper function to set attributes and add Nodes for an ID cell. |
| * @param {Element} td element to be added to current row in table. |
| * @param {dict} tableRow dictionary {'projectName': 'name', .. } of relevant row info. |
| * @param {boolean} isCrossProject are issues in the table from more than one project. |
| */ |
| function createIDCell(td, tableRow, isCrossProject) { |
| td.classList.add('id'); |
| let aLink = document.createElement('a'); |
| aLink.setAttribute('href', tableRow['issueCleanURL']); |
| aLink.setAttribute('class', 'computehref'); |
| let aLinkContent = (isCrossProject ? (tableRow['projectName'] + ':') : '' ) + tableRow['localID']; |
| aLink.textContent = aLinkContent; |
| td.appendChild(aLink); |
| } |
| |
| function createProjectCell(td, tableRow) { |
| td.classList.add('project'); |
| let aLink = document.createElement('a'); |
| aLink.setAttribute('href', tableRow['projectURL']); |
| aLink.textContent = tableRow['projectName']; |
| td.appendChild(aLink); |
| } |
| |
| function createEditableNoteCell(td, cell, projectName, localID, hotlistID) { |
| let textBox = document.createElement('textarea'); |
| setAttributes(textBox, { |
| 'id': `itemnote_${projectName}_${localID}`, |
| 'placeholder': '---', |
| 'class': 'itemnote rowwidgets', |
| 'projectname': projectName, |
| 'localid': localID, |
| 'style': 'height:15px', |
| }); |
| if (cell['values'].length > 0) { |
| textBox.value = cell['values'][0]['item']; |
| } |
| textBox.addEventListener('blur', function(e) { |
| saveNote(e.target, hotlistID); |
| }); |
| debouncedKeyHandler = debounce(function(e) { |
| saveNote(e.target, hotlistID); |
| }); |
| textBox.addEventListener('keyup', debouncedKeyHandler, false); |
| td.appendChild(textBox); |
| } |
| |
| function enter_detector(e) { |
| if (e.which==13||e.keyCode==13) { |
| this.blur(); |
| } |
| } |
| |
| |
| /** |
| * Helper function to set attributes and add Nodes for an Summary cell. |
| * @param {Element} td element to be added to current row in table. |
| * @param {dict} cell dictionary {'values': [], .. } of relevant cell info. |
| * @param {string=} projectName The name of the project the summary references. |
| */ |
| function createSummaryCell(td, cell, projectName) { |
| // TODO(jojwang): detect when links are present and make clicking on cell go |
| // to link, not issue details page |
| td.setAttribute('style', 'width:100%'); |
| fillValues(td, cell['values']); |
| fillNonColumnLabels(td, cell['nonColLabels'], projectName); |
| } |
| |
| |
| /** |
| * Helper function to set attributes and add Nodes for an Attribute or Unfilterable cell. |
| * @param {Element} td element to be added to current row in table. |
| * @param {dict} cell dictionary {'type': 'Summary', .. } of relevant cell info. |
| */ |
| function createAttrAndUnfiltCell(td, cell) { |
| if (cell['noWrap'] == 'yes') { |
| td.className += ' nowrapspan'; |
| } |
| if (cell['align']) { |
| td.setAttribute('align', cell['align']); |
| } |
| fillValues(td, cell['values']); |
| } |
| |
| function createUrlCell(td, cell) { |
| td.classList.add('url'); |
| cell.values.forEach((value) => { |
| let aLink = document.createElement('a'); |
| aLink.href = value['item']; |
| aLink.target = '_blank'; |
| aLink.rel = 'nofollow'; |
| aLink.textContent = value['item']; |
| aLink.classList.add('fieldvalue_url'); |
| td.appendChild(aLink); |
| }); |
| } |
| |
| function createIssuesCell(td, cell) { |
| td.classList.add('url'); |
| if (cell.values.length > 0) { |
| cell.values.forEach( function(value, index, array) { |
| const span = document.createElement('span'); |
| if (value['isDerived']) { |
| span.className = 'derived'; |
| } |
| const a = document.createElement('a'); |
| a.href = value['href']; |
| a.rel = 'nofollow"'; |
| if (value['title']) { |
| a.title = value['title']; |
| } |
| if (value['closed']) { |
| a.style.textDecoration = 'line-through'; |
| } |
| a.textContent = value['id']; |
| span.appendChild(a); |
| td.appendChild(span); |
| if (index != array.length-1) { |
| td.appendChild(document.createTextNode(', ')); |
| } |
| }); |
| } else { |
| td.textContent = '---'; |
| } |
| } |
| |
| /** |
| * Helper function to fill a td element with a cell's non-column labels. |
| * @param {Element} td element to be added to current row in table. |
| * @param {list} labels list of dictionaries with relevant (key, value) for |
| * each label |
| * @param {string=} projectName The name of the project the labels reference. |
| */ |
| function fillNonColumnLabels(td, labels, projectName) { |
| labels.forEach( function(label) { |
| const aLabel = document.createElement('a'); |
| setAttributes(aLabel, |
| { |
| 'class': 'label', |
| 'href': `/p/${projectName}/issues/list?q=label:${label['value']}`, |
| }); |
| if (label['isDerived']) { |
| const i = document.createElement('i'); |
| i.textContent = label['value']; |
| aLabel.appendChild(i); |
| } else { |
| aLabel.textContent = label['value']; |
| } |
| td.appendChild(document.createTextNode(' ')); |
| td.appendChild(aLabel); |
| }); |
| } |
| |
| |
| /** |
| * Helper function to fill a td element with a cell's value(s). |
| * @param {Element} td element to be added to current row in table. |
| * @param {list} values list of dictionaries with relevant (key, value) for each value |
| */ |
| function fillValues(td, values) { |
| if (values.length > 0) { |
| values.forEach( function(value, index, array) { |
| let span = document.createElement('span'); |
| if (value['isDerived']) { |
| span.className = 'derived'; |
| } |
| span.textContent = value['item']; |
| td.appendChild(span); |
| if (index != array.length-1) { |
| td.appendChild(document.createTextNode(', ')); |
| } |
| }); |
| } else { |
| td.textContent = '---'; |
| } |
| } |
| |
| |
| /** |
| * Helper function to create a table row. |
| * @param {dict} tableRow dictionary {'projectName': 'name', .. } of relevant row info. |
| * @param {dict} pageSettings dict of relevant settings for the hotlist and user viewing the page. |
| */ |
| function renderHotlistRow(tableRow, pageSettings) { |
| let tr = document.createElement('tr'); |
| if (pageSettings['cursor'] == tableRow['issueRef']) { |
| tr.setAttribute('class', 'ifOpened hoverTarget cursor_on drag_item'); |
| } else { |
| tr.setAttribute('class', 'ifOpened hoverTarget cursor_off drag_item'); |
| } |
| |
| setAttributes(tr, {'data-idx': tableRow['idx'], 'data-id': tableRow['issueID'], 'issue-context-url': tableRow['issueContextURL']}); |
| widgets = createWidgets(tableRow, pageSettings['readOnly'], |
| pageSettings['userLoggedIn']); |
| tr.appendChild(widgets); |
| tableRow['cells'].forEach(function(cell) { |
| let td = document.createElement('td'); |
| td.setAttribute('class', 'col_' + cell['colIndex']); |
| if (cell['type'] == 'ID') { |
| createIDCell(td, tableRow, (pageSettings['isCrossProject'] == 'True')); |
| } else if (cell['type'] == 'summary') { |
| createSummaryCell(td, cell, tableRow['projectName']); |
| } else if (cell['type'] == 'note') { |
| if (pageSettings['ownerPerm'] || pageSettings['editorPerm']) { |
| createEditableNoteCell( |
| td, cell, tableRow['projectName'], tableRow['localID'], |
| pageSettings['hotlistID']); |
| } else { |
| createSummaryCell(td, cell, tableRow['projectName']); |
| } |
| } else if (cell['type'] == 'project') { |
| createProjectCell(td, tableRow); |
| } else if (cell['type'] == 'url') { |
| createUrlCell(td, cell); |
| } else if (cell['type'] == 'issues') { |
| createIssuesCell(td, cell); |
| } else { |
| createAttrAndUnfiltCell(td, cell); |
| } |
| tr.appendChild(td); |
| }); |
| let directLinkURL = tableRow['issueCleanURL']; |
| let directLink = document.createElement('a'); |
| directLink.setAttribute('class', 'directlink material-icons'); |
| directLink.setAttribute('href', directLinkURL); |
| directLink.textContent = 'link'; // Renders as a link icon. |
| let lastCol = document.createElement('td'); |
| lastCol.appendChild(directLink); |
| tr.appendChild(lastCol); |
| return tr; |
| } |
| |
| |
| /** |
| * Helper function to create the group header row |
| * @param {dict} group dict of relevant values for the current group |
| * @return a <tr> element to be added to the current <tbody> |
| */ |
| function renderGroupRow(group) { |
| let tr = document.createElement('tr'); |
| tr.setAttribute('class', 'group_row'); |
| let td = document.createElement('td'); |
| setAttributes(td, {'colspan': '100', 'class': 'toggleHidden'}); |
| let whenClosedImg = document.createElement('img'); |
| setAttributes(whenClosedImg, {'class': 'ifClosed', 'src': '/static/images/plus.gif'}); |
| td.appendChild(whenClosedImg); |
| let whenOpenImg = document.createElement('img'); |
| setAttributes(whenOpenImg, {'class': 'ifOpened', 'src': '/static/images/minus.gif'}); |
| td.appendChild(whenOpenImg); |
| tr.appendChild(td); |
| |
| div = document.createElement('div'); |
| div.textContent += group['rowsInGroup']; |
| |
| div.textContent += (group['rowsInGroup'] == '1' ? ' issue:': ' issues:'); |
| |
| group['cells'].forEach(function(cell) { |
| let hasValue = false; |
| cell['values'].forEach(function(value) { |
| if (value['item'] !== 'None') { |
| hasValue = true; |
| } |
| }); |
| if (hasValue) { |
| cell.values.forEach(function(value) { |
| div.textContent += (' ' + cell['groupName'] + '=' + value['item']); |
| }); |
| } else { |
| div.textContent += (' -has:' + cell['groupName']); |
| } |
| }); |
| td.appendChild(div); |
| return tr; |
| } |
| |
| |
| /** |
| * Builds the body of a hotlistissues table. |
| * @param {dict} tableData dict of relevant values from 'table_data' |
| * @param {dict} pageSettings dict of relevant settings for the hotlist and user viewing the page. |
| */ |
| function renderHotlistTable(tableData, pageSettings) { |
| let tbody; |
| let table = $('resultstable'); |
| |
| // TODO(jojwang): this would not work if grouping did not require a page refresh |
| // that wiped the table of all its children. This should be redone to be more |
| // robust. |
| // This loop only does anything when reranking is enabled. |
| for (i=0; i < table.childNodes.length; i++) { |
| if (table.childNodes[i].tagName == 'TBODY') { |
| table.removeChild(table.childNodes[i]); |
| } |
| } |
| |
| tableData.forEach(function(tableRow) { |
| if (tableRow['group'] !== 'no') { |
| // add current tbody to table, need a new tbody with group row |
| if (typeof tbody !== 'undefined') { |
| table.appendChild(tbody); |
| } |
| tbody = document.createElement('tbody'); |
| tbody.setAttribute('class', 'opened'); |
| tbody.appendChild(renderGroupRow(tableRow['group'])); |
| } |
| if (typeof tbody == 'undefined') { |
| tbody = document.createElement('tbody'); |
| } |
| tbody.appendChild(renderHotlistRow(tableRow, pageSettings)); |
| }); |
| tbody.appendChild(document.createElement('tr')); |
| table.appendChild(tbody); |
| |
| let stars = document.getElementsByClassName('star'); |
| for (var i = 0; i < stars.length; ++i) { |
| let star = stars[i]; |
| star.addEventListener('click', function(event) { |
| let projectName = event.target.getAttribute('data-project-name'); |
| let localID = event.target.getAttribute('data-local-id'); |
| _TKR_toggleStar(event.target, projectName, localID, null, null, null); |
| }); |
| } |
| } |
| |
| |
| /** |
| * Activates the drag and drop functionality of the hotlistissues table. |
| * @param {dict} tableData dict of relevant values from the 'table_data' of |
| * hotlistissues servlet. This is used when a drag and drop motion does not |
| * result in any changes in the ordering of the issues. |
| * @param {dict} pageSettings dict of relevant settings for the hotlist and user |
| * viewing the page. |
| * @param {str} hotlistID the number ID of the current hotlist |
| */ |
| function activateDragDrop(tableData, pageSettings, hotlistID) { |
| function onHotlistRerank(srcID, targetID, position) { |
| let data = { |
| target_id: targetID, |
| moved_ids: srcID, |
| split_above: position == 'above', |
| colspec: pageSettings['colSpec'], |
| can: pageSettings['can'], |
| }; |
| CS_doPost(hotlistID + '/rerank.do', onHotlistResponse, data); |
| } |
| |
| function onHotlistResponse(event) { |
| let xhr = event.target; |
| if (xhr.readyState != 4) { |
| return; |
| } |
| if (xhr.status != 200) { |
| window.console.error('200 page error'); |
| // TODO(jojwang): fill this in more |
| return; |
| } |
| let response = CS_parseJSON(xhr); |
| renderHotlistTable( |
| (response['table_data'] == '' ? tableData : response['table_data']), |
| pageSettings); |
| // TODO(jojwang): pass pagination state to server |
| _initDragAndDrop($('resultstable'), onHotlistRerank, true); |
| } |
| _initDragAndDrop($('resultstable'), onHotlistRerank, true); |
| } |