blob: 8cdfcfb6bc1efcaf1aa8986bb43d124c74f7929f [file] [log] [blame]
// 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);
}