Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static/js/tracker/render-hotlist-table.js b/static/js/tracker/render-hotlist-table.js
new file mode 100644
index 0000000..5004296
--- /dev/null
+++ b/static/js/tracker/render-hotlist-table.js
@@ -0,0 +1,436 @@
+/* Copyright 2016 The Chromium Authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style
+ * license that can be found in the LICENSE file or at
+ * https://developers.google.com/open-source/licenses/bsd
+ */
+
+/**
+ * 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);
+}