blob: 5004296492b41f554f537425d88dc60c4b2ae8d3 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001/* 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 */
18function 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 */
34function 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*/
78function 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
88function 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
96function 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
119function 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*/
132function 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*/
146function 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
156function 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
169function 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 */
205function 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 */
231function 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 */
255function 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 */
310function 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 */
353function 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*/
406function 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}