blob: 8cdfcfb6bc1efcaf1aa8986bb43d124c74f7929f [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001// Copyright 2016 The Chromium Authors
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5/**
6 * This file contains JS functions used in rendering a hotlistissues table
7 */
8
9
10/**
11 * Helper function to set several attributes of an element at once.
12 * @param {Element} el element that is getting the attributes
13 * @param {dict} attrs Dictionary of {attrName: attrValue, ..}
14 */
15function setAttributes(el, attrs) {
16 for (let key in attrs) {
17 el.setAttribute(key, attrs[key]);
18 }
19}
20
21// TODO(jojwang): readOnly is currently empty string, figure out what it should be
22// ('True'/'False' 'yes'/'no'?).
23
24/**
25 * Helper function for creating a <td> element that contains the widgets of the row.
26 * @param {dict} tableRow dictionary {'projectName': 'name', .. } of relevant row info.
27 * @param {} readOnly.
28 * @param {boolean} userLoggedIn is the current user logged in.
29 * @return an element containing the widget elements
30 */
31function createWidgets(tableRow, readOnly, userLoggedIn) {
32 let widgets = document.createElement('td');
33 widgets.setAttribute('class', 'rowwidgets nowrap');
34
35 let gripper = document.createElement('i');
36 gripper.setAttribute('class', 'material-icons gripper');
37 gripper.setAttribute('title', 'Drag issue');
38 gripper.textContent = 'drag_indicator';
39 widgets.appendChild(gripper);
40
41 if (!readOnly) {
42 if (userLoggedIn) {
43 // TODO(jojwang): for bulk edit, only show a checkbox next to an issue that
44 // the user has permission to edit.
45 let checkbox = document.createElement('input');
46 setAttributes(checkbox, {'class': 'checkRangeSelect',
47 'id': 'cb_' + tableRow['issueRef'],
48 'type': 'checkbox'});
49 widgets.appendChild(checkbox);
50 widgets.appendChild(document.createTextNode(' '));
51
52 let star = document.createElement('a');
53 let starColor = tableRow['isStarred'] ? 'cornflowerblue' : 'gray';
54 let starred = tableRow['isStarred'] ? 'Un-s' : 'S';
55 setAttributes(star, {'class': 'star',
56 'id': 'star-' + tableRow['projectName'] + tableRow['localID'],
57 'style': 'color:' + starColor,
58 'title': starred + 'tar this issue',
59 'data-project-name': tableRow['projectName'],
60 'data-local-id': tableRow['localID']});
61 star.textContent = (tableRow['isStarred'] ? '\u2605' : '\u2606');
62 widgets.appendChild(star);
63 }
64 }
65 return widgets;
66}
67
68
69/**
70 * Helper function to set attributes and add Nodes for an ID cell.
71 * @param {Element} td element to be added to current row in table.
72 * @param {dict} tableRow dictionary {'projectName': 'name', .. } of relevant row info.
73 * @param {boolean} isCrossProject are issues in the table from more than one project.
74*/
75function createIDCell(td, tableRow, isCrossProject) {
76 td.classList.add('id');
77 let aLink = document.createElement('a');
78 aLink.setAttribute('href', tableRow['issueCleanURL']);
79 aLink.setAttribute('class', 'computehref');
80 let aLinkContent = (isCrossProject ? (tableRow['projectName'] + ':') : '' ) + tableRow['localID'];
81 aLink.textContent = aLinkContent;
82 td.appendChild(aLink);
83}
84
85function createProjectCell(td, tableRow) {
86 td.classList.add('project');
87 let aLink = document.createElement('a');
88 aLink.setAttribute('href', tableRow['projectURL']);
89 aLink.textContent = tableRow['projectName'];
90 td.appendChild(aLink);
91}
92
93function createEditableNoteCell(td, cell, projectName, localID, hotlistID) {
94 let textBox = document.createElement('textarea');
95 setAttributes(textBox, {
96 'id': `itemnote_${projectName}_${localID}`,
97 'placeholder': '---',
98 'class': 'itemnote rowwidgets',
99 'projectname': projectName,
100 'localid': localID,
101 'style': 'height:15px',
102 });
103 if (cell['values'].length > 0) {
104 textBox.value = cell['values'][0]['item'];
105 }
106 textBox.addEventListener('blur', function(e) {
107 saveNote(e.target, hotlistID);
108 });
109 debouncedKeyHandler = debounce(function(e) {
110 saveNote(e.target, hotlistID);
111 });
112 textBox.addEventListener('keyup', debouncedKeyHandler, false);
113 td.appendChild(textBox);
114}
115
116function enter_detector(e) {
117 if (e.which==13||e.keyCode==13) {
118 this.blur();
119 }
120}
121
122
123/**
124 * Helper function to set attributes and add Nodes for an Summary cell.
125 * @param {Element} td element to be added to current row in table.
126 * @param {dict} cell dictionary {'values': [], .. } of relevant cell info.
127 * @param {string=} projectName The name of the project the summary references.
128*/
129function createSummaryCell(td, cell, projectName) {
130 // TODO(jojwang): detect when links are present and make clicking on cell go
131 // to link, not issue details page
132 td.setAttribute('style', 'width:100%');
133 fillValues(td, cell['values']);
134 fillNonColumnLabels(td, cell['nonColLabels'], projectName);
135}
136
137
138/**
139 * Helper function to set attributes and add Nodes for an Attribute or Unfilterable cell.
140 * @param {Element} td element to be added to current row in table.
141 * @param {dict} cell dictionary {'type': 'Summary', .. } of relevant cell info.
142*/
143function createAttrAndUnfiltCell(td, cell) {
144 if (cell['noWrap'] == 'yes') {
145 td.className += ' nowrapspan';
146 }
147 if (cell['align']) {
148 td.setAttribute('align', cell['align']);
149 }
150 fillValues(td, cell['values']);
151}
152
153function createUrlCell(td, cell) {
154 td.classList.add('url');
155 cell.values.forEach((value) => {
156 let aLink = document.createElement('a');
157 aLink.href = value['item'];
158 aLink.target = '_blank';
159 aLink.rel = 'nofollow';
160 aLink.textContent = value['item'];
161 aLink.classList.add('fieldvalue_url');
162 td.appendChild(aLink);
163 });
164}
165
166function createIssuesCell(td, cell) {
167 td.classList.add('url');
168 if (cell.values.length > 0) {
169 cell.values.forEach( function(value, index, array) {
170 const span = document.createElement('span');
171 if (value['isDerived']) {
172 span.className = 'derived';
173 }
174 const a = document.createElement('a');
175 a.href = value['href'];
176 a.rel = 'nofollow"';
177 if (value['title']) {
178 a.title = value['title'];
179 }
180 if (value['closed']) {
181 a.style.textDecoration = 'line-through';
182 }
183 a.textContent = value['id'];
184 span.appendChild(a);
185 td.appendChild(span);
186 if (index != array.length-1) {
187 td.appendChild(document.createTextNode(', '));
188 }
189 });
190 } else {
191 td.textContent = '---';
192 }
193}
194
195/**
196 * Helper function to fill a td element with a cell's non-column labels.
197 * @param {Element} td element to be added to current row in table.
198 * @param {list} labels list of dictionaries with relevant (key, value) for
199 * each label
200 * @param {string=} projectName The name of the project the labels reference.
201 */
202function fillNonColumnLabels(td, labels, projectName) {
203 labels.forEach( function(label) {
204 const aLabel = document.createElement('a');
205 setAttributes(aLabel,
206 {
207 'class': 'label',
208 'href': `/p/${projectName}/issues/list?q=label:${label['value']}`,
209 });
210 if (label['isDerived']) {
211 const i = document.createElement('i');
212 i.textContent = label['value'];
213 aLabel.appendChild(i);
214 } else {
215 aLabel.textContent = label['value'];
216 }
217 td.appendChild(document.createTextNode(' '));
218 td.appendChild(aLabel);
219 });
220}
221
222
223/**
224 * Helper function to fill a td element with a cell's value(s).
225 * @param {Element} td element to be added to current row in table.
226 * @param {list} values list of dictionaries with relevant (key, value) for each value
227 */
228function fillValues(td, values) {
229 if (values.length > 0) {
230 values.forEach( function(value, index, array) {
231 let span = document.createElement('span');
232 if (value['isDerived']) {
233 span.className = 'derived';
234 }
235 span.textContent = value['item'];
236 td.appendChild(span);
237 if (index != array.length-1) {
238 td.appendChild(document.createTextNode(', '));
239 }
240 });
241 } else {
242 td.textContent = '---';
243 }
244}
245
246
247/**
248 * Helper function to create a table row.
249 * @param {dict} tableRow dictionary {'projectName': 'name', .. } of relevant row info.
250 * @param {dict} pageSettings dict of relevant settings for the hotlist and user viewing the page.
251 */
252function renderHotlistRow(tableRow, pageSettings) {
253 let tr = document.createElement('tr');
254 if (pageSettings['cursor'] == tableRow['issueRef']) {
255 tr.setAttribute('class', 'ifOpened hoverTarget cursor_on drag_item');
256 } else {
257 tr.setAttribute('class', 'ifOpened hoverTarget cursor_off drag_item');
258 }
259
260 setAttributes(tr, {'data-idx': tableRow['idx'], 'data-id': tableRow['issueID'], 'issue-context-url': tableRow['issueContextURL']});
261 widgets = createWidgets(tableRow, pageSettings['readOnly'],
262 pageSettings['userLoggedIn']);
263 tr.appendChild(widgets);
264 tableRow['cells'].forEach(function(cell) {
265 let td = document.createElement('td');
266 td.setAttribute('class', 'col_' + cell['colIndex']);
267 if (cell['type'] == 'ID') {
268 createIDCell(td, tableRow, (pageSettings['isCrossProject'] == 'True'));
269 } else if (cell['type'] == 'summary') {
270 createSummaryCell(td, cell, tableRow['projectName']);
271 } else if (cell['type'] == 'note') {
272 if (pageSettings['ownerPerm'] || pageSettings['editorPerm']) {
273 createEditableNoteCell(
274 td, cell, tableRow['projectName'], tableRow['localID'],
275 pageSettings['hotlistID']);
276 } else {
277 createSummaryCell(td, cell, tableRow['projectName']);
278 }
279 } else if (cell['type'] == 'project') {
280 createProjectCell(td, tableRow);
281 } else if (cell['type'] == 'url') {
282 createUrlCell(td, cell);
283 } else if (cell['type'] == 'issues') {
284 createIssuesCell(td, cell);
285 } else {
286 createAttrAndUnfiltCell(td, cell);
287 }
288 tr.appendChild(td);
289 });
290 let directLinkURL = tableRow['issueCleanURL'];
291 let directLink = document.createElement('a');
292 directLink.setAttribute('class', 'directlink material-icons');
293 directLink.setAttribute('href', directLinkURL);
294 directLink.textContent = 'link'; // Renders as a link icon.
295 let lastCol = document.createElement('td');
296 lastCol.appendChild(directLink);
297 tr.appendChild(lastCol);
298 return tr;
299}
300
301
302/**
303 * Helper function to create the group header row
304 * @param {dict} group dict of relevant values for the current group
305 * @return a <tr> element to be added to the current <tbody>
306 */
307function renderGroupRow(group) {
308 let tr = document.createElement('tr');
309 tr.setAttribute('class', 'group_row');
310 let td = document.createElement('td');
311 setAttributes(td, {'colspan': '100', 'class': 'toggleHidden'});
312 let whenClosedImg = document.createElement('img');
313 setAttributes(whenClosedImg, {'class': 'ifClosed', 'src': '/static/images/plus.gif'});
314 td.appendChild(whenClosedImg);
315 let whenOpenImg = document.createElement('img');
316 setAttributes(whenOpenImg, {'class': 'ifOpened', 'src': '/static/images/minus.gif'});
317 td.appendChild(whenOpenImg);
318 tr.appendChild(td);
319
320 div = document.createElement('div');
321 div.textContent += group['rowsInGroup'];
322
323 div.textContent += (group['rowsInGroup'] == '1' ? ' issue:': ' issues:');
324
325 group['cells'].forEach(function(cell) {
326 let hasValue = false;
327 cell['values'].forEach(function(value) {
328 if (value['item'] !== 'None') {
329 hasValue = true;
330 }
331 });
332 if (hasValue) {
333 cell.values.forEach(function(value) {
334 div.textContent += (' ' + cell['groupName'] + '=' + value['item']);
335 });
336 } else {
337 div.textContent += (' -has:' + cell['groupName']);
338 }
339 });
340 td.appendChild(div);
341 return tr;
342}
343
344
345/**
346 * Builds the body of a hotlistissues table.
347 * @param {dict} tableData dict of relevant values from 'table_data'
348 * @param {dict} pageSettings dict of relevant settings for the hotlist and user viewing the page.
349 */
350function renderHotlistTable(tableData, pageSettings) {
351 let tbody;
352 let table = $('resultstable');
353
354 // TODO(jojwang): this would not work if grouping did not require a page refresh
355 // that wiped the table of all its children. This should be redone to be more
356 // robust.
357 // This loop only does anything when reranking is enabled.
358 for (i=0; i < table.childNodes.length; i++) {
359 if (table.childNodes[i].tagName == 'TBODY') {
360 table.removeChild(table.childNodes[i]);
361 }
362 }
363
364 tableData.forEach(function(tableRow) {
365 if (tableRow['group'] !== 'no') {
366 // add current tbody to table, need a new tbody with group row
367 if (typeof tbody !== 'undefined') {
368 table.appendChild(tbody);
369 }
370 tbody = document.createElement('tbody');
371 tbody.setAttribute('class', 'opened');
372 tbody.appendChild(renderGroupRow(tableRow['group']));
373 }
374 if (typeof tbody == 'undefined') {
375 tbody = document.createElement('tbody');
376 }
377 tbody.appendChild(renderHotlistRow(tableRow, pageSettings));
378 });
379 tbody.appendChild(document.createElement('tr'));
380 table.appendChild(tbody);
381
382 let stars = document.getElementsByClassName('star');
383 for (var i = 0; i < stars.length; ++i) {
384 let star = stars[i];
385 star.addEventListener('click', function(event) {
386 let projectName = event.target.getAttribute('data-project-name');
387 let localID = event.target.getAttribute('data-local-id');
388 _TKR_toggleStar(event.target, projectName, localID, null, null, null);
389 });
390 }
391}
392
393
394/**
395 * Activates the drag and drop functionality of the hotlistissues table.
396 * @param {dict} tableData dict of relevant values from the 'table_data' of
397 * hotlistissues servlet. This is used when a drag and drop motion does not
398 * result in any changes in the ordering of the issues.
399 * @param {dict} pageSettings dict of relevant settings for the hotlist and user
400 * viewing the page.
401 * @param {str} hotlistID the number ID of the current hotlist
402*/
403function activateDragDrop(tableData, pageSettings, hotlistID) {
404 function onHotlistRerank(srcID, targetID, position) {
405 let data = {
406 target_id: targetID,
407 moved_ids: srcID,
408 split_above: position == 'above',
409 colspec: pageSettings['colSpec'],
410 can: pageSettings['can'],
411 };
412 CS_doPost(hotlistID + '/rerank.do', onHotlistResponse, data);
413 }
414
415 function onHotlistResponse(event) {
416 let xhr = event.target;
417 if (xhr.readyState != 4) {
418 return;
419 }
420 if (xhr.status != 200) {
421 window.console.error('200 page error');
422 // TODO(jojwang): fill this in more
423 return;
424 }
425 let response = CS_parseJSON(xhr);
426 renderHotlistTable(
427 (response['table_data'] == '' ? tableData : response['table_data']),
428 pageSettings);
429 // TODO(jojwang): pass pagination state to server
430 _initDragAndDrop($('resultstable'), onHotlistRerank, true);
431 }
432 _initDragAndDrop($('resultstable'), onHotlistRerank, true);
433}