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);
+}