blob: bf898ce48b62d1ffdcee4ec2604321a5a2d9fd4c [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/* eslint-disable camelcase */
5/* eslint-disable no-unused-vars */
6
7/**
8 * This file contains the autocomplete configuration logic that is
9 * specific to the issue fields of Monorail. It depends on ac.js, our
10 * modified version of the autocomplete library.
11 */
12
13/**
14 * This is an autocomplete store that holds the hotlists of the current user.
15 */
16let TKR_hotlistsStore;
17
18/**
19 * This is an autocomplete store that holds well-known issue label
20 * values for the current project.
21 */
22let TKR_labelStore;
23
24/**
25 * Like TKR_labelStore but stores only label prefixes.
26 */
27let TKR_labelPrefixStore;
28
29/**
30 * Like TKR_labelStore but adds a trailing comma instead of replacing.
31 */
32let TKR_labelMultiStore;
33
34/**
35 * This is an autocomplete store that holds issue components.
36 */
37let TKR_componentStore;
38
39/**
40 * Like TKR_componentStore but adds a trailing comma instead of replacing.
41 */
42let TKR_componentListStore;
43
44/**
45 * This is an autocomplete store that holds many different kinds of
46 * items that can be shown in the artifact search autocomplete.
47 */
48let TKR_searchStore;
49
50/**
51 * This is similar to TKR_searchStore, but does not include any suggestions
52 * to use the "me" keyword. Using "me" is not a good idea for project canned
53 * queries and filter rules.
54 */
55let TKR_projectQueryStore;
56
57/**
58 * This is an autocomplete store that holds items for the quick edit
59 * autocomplete.
60 */
61// TODO(jrobbins): add options for fields and components.
62let TKR_quickEditStore;
63
64/**
65 * This is a list of label prefixes that each issue should only use once.
66 * E.g., each issue should only have one Priority-* label. We do not prevent
67 * the user from using multiple such labels, we just warn the user before
68 * they submit.
69 */
70let TKR_exclPrefixes = [];
71
72/**
73 * This is an autocomplete store that holds custom permission names that
74 * have already been used in this project.
75 */
76let TKR_customPermissionsStore;
77
78
79/**
80 * This is an autocomplete store that holds well-known issue status
81 * values for the current project.
82 */
83let TKR_statusStore;
84
85
86/**
87 * This is an autocomplete store that holds the usernames of all the
88 * members of the current project. This is used for autocomplete in
89 * the cc-list of an issue, where many user names can entered with
90 * commas between them.
91 */
92let TKR_memberListStore;
93
94
95/**
96 * This is an autocomplete store that holds the projects that the current
97 * user is contributor/member/owner of.
98 */
99let TKR_projectStore;
100
101/**
102 * This is an autocomplete store that holds the usernames of possible
103 * issue owners in the current project. The list of possible issue
104 * owners is the same as the list of project members, but the behavior
105 * of this autocompete store is different because the issue owner text
106 * field can only accept one value.
107 */
108let TKR_ownerStore;
109
110
111/**
112 * This is an autocomplete store that holds any list of string for choices.
113 */
114let TKR_autoCompleteStore;
115
116
117/**
118 * An array of autocomplete stores used for user-type custom fields.
119 */
120const TKR_userAutocompleteStores = [];
121
122
123/**
124 * This boolean controls whether odd-ball status and labels are treated as
125 * a warning or an error. Normally, it is False.
126 */
127// TODO(jrobbins): split this into one option for statuses and one for labels.
128let TKR_restrict_to_known;
129
130/**
131 * This substitute function should be used for multi-valued autocomplete fields
132 * that are delimited by commas. When we insert an autocomplete value, replace
133 * an entire search term. Add a comma and a space after it if it is a complete
134 * search term.
135 */
136function TKR_acSubstituteWithComma(inputValue, caret, completable, completion) {
137 let nextTerm = caret;
138
139 // Subtract one in case the cursor is at the end of the input, before a comma.
140 let prevTerm = caret - 1;
141 while (nextTerm < inputValue.length - 1 && inputValue.charAt(nextTerm) !== ',') {
142 nextTerm++;
143 }
144 // Set this at the position after the found comma.
145 nextTerm++;
146
147 while (prevTerm > 0 && ![',', ' '].includes(inputValue.charAt(prevTerm))) {
148 prevTerm--;
149 }
150 if (prevTerm > 0) {
151 // Set this boundary after the found space/comma if it's not the beginning
152 // of the field.
153 prevTerm++;
154 }
155
156 return inputValue.substring(0, prevTerm) +
157 completion.value + ', ' + inputValue.substring(nextTerm);
158}
159
160/**
161 * When the prefix starts with '*', return the complete set of all
162 * possible completions.
163 * @param {string} prefix If this starts with '*', return all possible
164 * completions. Otherwise return null.
165 * @param {Array} labelDefs The array of label names and docstrings.
166 * @return Array of new _AC_Completions for each possible completion, or null.
167 */
168function TKR_fullComplete(prefix, labelDefs) {
169 if (!prefix.startsWith('*')) return null;
170 const out = [];
171 for (let i = 0; i < labelDefs.length; i++) {
172 out.push(new _AC_Completion(labelDefs[i].name,
173 labelDefs[i].name,
174 labelDefs[i].doc));
175 }
176 return out;
177}
178
179
180/**
181 * Constucts a list of all completions for both open and closed
182 * statuses, with a header for each group.
183 * @param {string} prefix If starts with '*', return all possible completions,
184 * else return null.
185 * @param {Array} openStatusDefs The array of open status values and
186 * docstrings.
187 * @param {Array} closedStatusDefs The array of closed status values
188 * and docstrings.
189 * @return Array of new _AC_Completions for each possible completion, or null.
190 */
191function TKR_openClosedComplete(prefix, openStatusDefs, closedStatusDefs) {
192 if (!prefix.startsWith('*')) return null;
193 const out = [];
194 out.push({heading: 'Open Statuses:'}); // TODO: i18n
195 for (var i = 0; i < openStatusDefs.length; i++) {
196 out.push(new _AC_Completion(openStatusDefs[i].name,
197 openStatusDefs[i].name,
198 openStatusDefs[i].doc));
199 }
200 out.push({heading: 'Closed Statuses:'}); // TODO: i18n
201 for (var i = 0; i < closedStatusDefs.length; i++) {
202 out.push(new _AC_Completion(closedStatusDefs[i].name,
203 closedStatusDefs[i].name,
204 closedStatusDefs[i].doc));
205 }
206 return out;
207}
208
209
210function TKR_setUpHotlistsStore(hotlists) {
211 const docdict = {};
212 const ref_strs = [];
213
214 for (let i = 0; i < hotlists.length; i++) {
215 ref_strs.push(hotlists[i]['ref_str']);
216 docdict[hotlists[i]['ref_str']] = hotlists[i]['summary'];
217 }
218
219 TKR_hotlistsStore = new _AC_SimpleStore(ref_strs, docdict);
220 TKR_hotlistsStore.substitute = TKR_acSubstituteWithComma;
221}
222
223
224/**
225 * An array of definitions of all well-known issue statuses. Each
226 * definition has the name of the status value, and a docstring that
227 * describes its meaning.
228 */
229let TKR_statusWords = [];
230
231
232/**
233 * Constuct a new autocomplete store with all the well-known issue
234 * status values. The store has some DIT-specific methods.
235 * TODO(jrobbins): would it be easier to define my own class to use
236 * instead of _AC_Simple_Store?
237 * @param {Array} openStatusDefs An array of definitions of the
238 * well-known open status values. Each definition has a name and
239 * docstring.
240 * @param {Array} closedStatusDefs An array of definitions of the
241 * well-known closed status values. Each definition has a name and
242 * docstring.
243 */
244function TKR_setUpStatusStore(openStatusDefs, closedStatusDefs) {
245 const docdict = {};
246 TKR_statusWords = [];
247 for (var i = 0; i < openStatusDefs.length; i++) {
248 var status = openStatusDefs[i];
249 TKR_statusWords.push(status.name);
250 docdict[status.name] = status.doc;
251 }
252 for (var i = 0; i < closedStatusDefs.length; i++) {
253 var status = closedStatusDefs[i];
254 TKR_statusWords.push(status.name);
255 docdict[status.name] = status.doc;
256 }
257
258 TKR_statusStore = new _AC_SimpleStore(TKR_statusWords, docdict);
259
260 TKR_statusStore.commaCompletes = false;
261
262 TKR_statusStore.substitute =
263 function(inputValue, cursor, completable, completion) {
264 return completion.value;
265 };
266
267 TKR_statusStore.completable = function(inputValue, cursor) {
268 if (!ac_everTyped) return '*status';
269 return inputValue;
270 };
271
272 TKR_statusStore.completions = function(prefix, tofilter) {
273 const fullList = TKR_openClosedComplete(prefix,
274 openStatusDefs,
275 closedStatusDefs);
276 if (fullList) return fullList;
277 return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
278 };
279}
280
281
282/**
283 * Simple function to add a given item to the list of items used to construct
284 * an "autocomplete store", and also update the docstring that describes
285 * that item. They are stored separately for backward compatability with
286 * autocomplete store logic that preceeded the introduction of descriptions.
287 */
288function TKR_addACItem(items, docDict, item, docStr) {
289 items.push(item);
290 docDict[item] = docStr;
291}
292
293/**
294 * Adds a group of three items related to a date field.
295 */
296function TKR_addACDateItems(items, docDict, fieldName, humanReadable) {
297 const today = new Date();
298 const todayStr = (today.getFullYear() + '-' + (today.getMonth() + 1) + '-' +
299 today.getDate());
300 TKR_addACItem(items, docDict, fieldName + '>today-1',
301 humanReadable + ' within the last N days');
302 TKR_addACItem(items, docDict, fieldName + '>' + todayStr,
303 humanReadable + ' after the specified date');
304 TKR_addACItem(items, docDict, fieldName + '<today-1',
305 humanReadable + ' more than N days ago');
306}
307
308/**
309 * Add several autocomplete items to a word list that will be used to construct
310 * an autocomplete store. Also, keep track of description strings for each
311 * item. A search operator is prepended to the name of each item. The opt_old
312 * and opt_new parameters are used to transform Key-Value labels into Key=Value
313 * search terms.
314 */
315function TKR_addACItemList(
316 items, docDict, searchOp, acDefs, opt_old, opt_new) {
317 let item;
318 for (let i = 0; i < acDefs.length; i++) {
319 const nameAndDoc = acDefs[i];
320 item = searchOp + nameAndDoc.name;
321 if (opt_old) {
322 // Preserve any leading minus-sign.
323 item = item.slice(0, 1) + item.slice(1).replace(opt_old, opt_new);
324 }
325 TKR_addACItem(items, docDict, item, nameAndDoc.doc);
326 }
327}
328
329
330/**
331 * Use information from an options feed to populate the artifact search
332 * autocomplete menu. The order of sections is: custom fields, labels,
333 * components, people, status, special, dates. Within each section,
334 * options are ordered semantically where possible, or alphabetically
335 * if there is no semantic ordering. Negated options all come after
336 * all normal options.
337 */
338function TKR_setUpSearchStore(
339 labelDefs, memberDefs, openDefs, closedDefs, componentDefs, fieldDefs,
340 indMemberDefs) {
341 let searchWords = [];
342 const searchWordsNeg = [];
343 const docDict = {};
344
345 // Treat Key-Value and OneWord labels separately.
346 const keyValueLabelDefs = [];
347 const oneWordLabelDefs = [];
348 for (var i = 0; i < labelDefs.length; i++) {
349 const nameAndDoc = labelDefs[i];
350 if (nameAndDoc.name.indexOf('-') == -1) {
351 oneWordLabelDefs.push(nameAndDoc);
352 } else {
353 keyValueLabelDefs.push(nameAndDoc);
354 }
355 }
356
357 // Autocomplete for custom fields.
358 for (i = 0; i < fieldDefs.length; i++) {
359 const fieldName = fieldDefs[i]['field_name'];
360 const fieldType = fieldDefs[i]['field_type'];
361 if (fieldType == 'ENUM_TYPE') {
362 const choices = fieldDefs[i]['choices'];
363 TKR_addACItemList(searchWords, docDict, fieldName + '=', choices);
364 TKR_addACItemList(searchWordsNeg, docDict, '-' + fieldName + '=', choices);
365 } else if (fieldType == 'STR_TYPE') {
366 TKR_addACItem(searchWords, docDict, fieldName + ':',
367 fieldDefs[i]['docstring']);
368 } else if (fieldType == 'DATE_TYPE') {
369 TKR_addACItem(searchWords, docDict, fieldName + ':',
370 fieldDefs[i]['docstring']);
371 TKR_addACDateItems(searchWords, docDict, fieldName, fieldName);
372 } else {
373 TKR_addACItem(searchWords, docDict, fieldName + '=',
374 fieldDefs[i]['docstring']);
375 }
376 TKR_addACItem(searchWords, docDict, 'has:' + fieldName,
377 'Issues with any ' + fieldName + ' value');
378 TKR_addACItem(searchWordsNeg, docDict, '-has:' + fieldName,
379 'Issues with no ' + fieldName + ' value');
380 }
381
382 // Add suggestions with "me" first, because otherwise they may be impossible
383 // to reach in a project that has a lot of members with emails starting with
384 // "me".
385 if (CS_env['loggedInUserEmail']) {
386 TKR_addACItem(searchWords, docDict, 'owner:me', 'Issues owned by me');
387 TKR_addACItem(searchWordsNeg, docDict, '-owner:me', 'Issues not owned by me');
388 TKR_addACItem(searchWords, docDict, 'cc:me', 'Issues that CC me');
389 TKR_addACItem(searchWordsNeg, docDict, '-cc:me', 'Issues that don\'t CC me');
390 TKR_addACItem(searchWords, docDict, 'reporter:me', 'Issues I reported');
391 TKR_addACItem(searchWordsNeg, docDict, '-reporter:me', 'Issues reported by others');
392 TKR_addACItem(searchWords, docDict, 'commentby:me',
393 'Issues that I commented on');
394 TKR_addACItem(searchWordsNeg, docDict, '-commentby:me',
395 'Issues that I didn\'t comment on');
396 }
397
398 TKR_addACItemList(searchWords, docDict, '', keyValueLabelDefs, '-', '=');
399 TKR_addACItemList(searchWordsNeg, docDict, '-', keyValueLabelDefs, '-', '=');
400 TKR_addACItemList(searchWords, docDict, 'label:', oneWordLabelDefs);
401 TKR_addACItemList(searchWordsNeg, docDict, '-label:', oneWordLabelDefs);
402
403 TKR_addACItemList(searchWords, docDict, 'component:', componentDefs);
404 TKR_addACItemList(searchWordsNeg, docDict, '-component:', componentDefs);
405 TKR_addACItem(searchWords, docDict, 'has:component',
406 'Issues with any components specified');
407 TKR_addACItem(searchWordsNeg, docDict, '-has:component',
408 'Issues with no components specified');
409
410 TKR_addACItemList(searchWords, docDict, 'owner:', indMemberDefs);
411 TKR_addACItemList(searchWordsNeg, docDict, '-owner:', indMemberDefs);
412 TKR_addACItemList(searchWords, docDict, 'cc:', memberDefs);
413 TKR_addACItemList(searchWordsNeg, docDict, '-cc:', memberDefs);
414 TKR_addACItem(searchWords, docDict, 'has:cc',
415 'Issues with any cc\'d users');
416 TKR_addACItem(searchWordsNeg, docDict, '-has:cc',
417 'Issues with no cc\'d users');
418 TKR_addACItemList(searchWords, docDict, 'reporter:', memberDefs);
419 TKR_addACItemList(searchWordsNeg, docDict, '-reporter:', memberDefs);
420 TKR_addACItemList(searchWords, docDict, 'status:', openDefs);
421 TKR_addACItemList(searchWordsNeg, docDict, '-status:', openDefs);
422 TKR_addACItemList(searchWords, docDict, 'status:', closedDefs);
423 TKR_addACItemList(searchWordsNeg, docDict, '-status:', closedDefs);
424 TKR_addACItem(searchWords, docDict, 'has:status',
425 'Issues with any status');
426 TKR_addACItem(searchWordsNeg, docDict, '-has:status',
427 'Issues with no status');
428
429 TKR_addACItem(searchWords, docDict, 'is:blocked',
430 'Issues that are blocked');
431 TKR_addACItem(searchWordsNeg, docDict, '-is:blocked',
432 'Issues that are not blocked');
433 TKR_addACItem(searchWords, docDict, 'has:blockedon',
434 'Issues that are blocked');
435 TKR_addACItem(searchWordsNeg, docDict, '-has:blockedon',
436 'Issues that are not blocked');
437 TKR_addACItem(searchWords, docDict, 'has:blocking',
438 'Issues that are blocking other issues');
439 TKR_addACItem(searchWordsNeg, docDict, '-has:blocking',
440 'Issues that are not blocking other issues');
441 TKR_addACItem(searchWords, docDict, 'has:mergedinto',
442 'Issues that were merged into other issues');
443 TKR_addACItem(searchWordsNeg, docDict, '-has:mergedinto',
444 'Issues that were not merged into other issues');
445
446 TKR_addACItem(searchWords, docDict, 'is:starred',
447 'Starred by me');
448 TKR_addACItem(searchWordsNeg, docDict, '-is:starred',
449 'Not starred by me');
450 TKR_addACItem(searchWords, docDict, 'stars>10',
451 'More than 10 stars');
452 TKR_addACItem(searchWords, docDict, 'stars>100',
453 'More than 100 stars');
454 TKR_addACItem(searchWords, docDict, 'summary:',
455 'Search within the summary field');
456
457 TKR_addACItemList(searchWords, docDict, 'commentby:', memberDefs);
458 TKR_addACItem(searchWords, docDict, 'attachment:',
459 'Search within attachment names');
460 TKR_addACItem(searchWords, docDict, 'attachments>5',
461 'Has more than 5 attachments');
462 TKR_addACItem(searchWords, docDict, 'is:open', 'Issues that are open');
463 TKR_addACItem(searchWordsNeg, docDict, '-is:open', 'Issues that are closed');
464 TKR_addACItem(searchWords, docDict, 'has:owner',
465 'Issues with some owner');
466 TKR_addACItem(searchWordsNeg, docDict, '-has:owner',
467 'Issues with no owner');
468 TKR_addACItem(searchWords, docDict, 'has:attachments',
469 'Issues with some attachments');
470 TKR_addACItem(searchWords, docDict, 'id:1,2,3',
471 'Match only the specified issues');
472 TKR_addACItem(searchWords, docDict, 'id<100000',
473 'Issues with IDs under 100,000');
474 TKR_addACItem(searchWords, docDict, 'blockedon:1',
475 'Blocked on the specified issues');
476 TKR_addACItem(searchWords, docDict, 'blocking:1',
477 'Blocking the specified issues');
478 TKR_addACItem(searchWords, docDict, 'mergedinto:1',
479 'Merged into the specified issues');
480 TKR_addACItem(searchWords, docDict, 'is:ownerbouncing',
481 'Issues with owners we cannot contact');
482 TKR_addACItem(searchWords, docDict, 'is:spam', 'Issues classified as spam');
483 // We do not suggest -is:spam because it is implicit.
484
485 TKR_addACDateItems(searchWords, docDict, 'opened', 'Opened');
486 TKR_addACDateItems(searchWords, docDict, 'modified', 'Modified');
487 TKR_addACDateItems(searchWords, docDict, 'closed', 'Closed');
488 TKR_addACDateItems(searchWords, docDict, 'ownermodified', 'Owner field modified');
489 TKR_addACDateItems(searchWords, docDict, 'ownerlastvisit', 'Owner last visit');
490 TKR_addACDateItems(searchWords, docDict, 'statusmodified', 'Status field modified');
491 TKR_addACDateItems(
492 searchWords, docDict, 'componentmodified', 'Component field modified');
493
494 TKR_projectQueryStore = new _AC_SimpleStore(searchWords, docDict);
495
496 searchWords = searchWords.concat(searchWordsNeg);
497
498 TKR_searchStore = new _AC_SimpleStore(searchWords, docDict);
499
500 // When we insert an autocomplete value, replace an entire search term.
501 // Add just a space after it (not a comma) if it is a complete search term,
502 // or leave the caret immediately after the completion if we are just helping
503 // the user with the search operator.
504 TKR_searchStore.substitute =
505 function(inputValue, caret, completable, completion) {
506 let nextTerm = caret;
507 while (inputValue.charAt(nextTerm) != ' ' &&
508 nextTerm < inputValue.length) {
509 nextTerm++;
510 }
511 while (inputValue.charAt(nextTerm) == ' ' &&
512 nextTerm < inputValue.length) {
513 nextTerm++;
514 }
515 return inputValue.substring(0, caret - completable.length) +
516 completion.value + ' ' + inputValue.substring(nextTerm);
517 };
518 TKR_searchStore.autoselectFirstRow =
519 function() {
520 return false;
521 };
522
523 TKR_projectQueryStore.substitute = TKR_searchStore.substitute;
524 TKR_projectQueryStore.autoselectFirstRow = TKR_searchStore.autoselectFirstRow;
525}
526
527
528/**
529 * Use information from an options feed to populate the issue quick edit
530 * autocomplete menu.
531 */
532function TKR_setUpQuickEditStore(
533 labelDefs, memberDefs, openDefs, closedDefs, indMemberDefs) {
534 const qeWords = [];
535 const docDict = {};
536
537 // Treat Key-Value and OneWord labels separately.
538 const keyValueLabelDefs = [];
539 const oneWordLabelDefs = [];
540 for (let i = 0; i < labelDefs.length; i++) {
541 const nameAndDoc = labelDefs[i];
542 if (nameAndDoc.name.indexOf('-') == -1) {
543 oneWordLabelDefs.push(nameAndDoc);
544 } else {
545 keyValueLabelDefs.push(nameAndDoc);
546 }
547 }
548 TKR_addACItemList(qeWords, docDict, '', keyValueLabelDefs, '-', '=');
549 TKR_addACItemList(qeWords, docDict, '-', keyValueLabelDefs, '-', '=');
550 TKR_addACItemList(qeWords, docDict, '', oneWordLabelDefs);
551 TKR_addACItemList(qeWords, docDict, '-', oneWordLabelDefs);
552
553 TKR_addACItem(qeWords, docDict, 'owner=me', 'Make me the owner');
554 TKR_addACItem(qeWords, docDict, 'owner=----', 'Clear the owner field');
555 TKR_addACItem(qeWords, docDict, 'cc=me', 'CC me on this issue');
556 TKR_addACItem(qeWords, docDict, 'cc=-me', 'Remove me from CC list');
557 TKR_addACItemList(qeWords, docDict, 'owner=', indMemberDefs);
558 TKR_addACItemList(qeWords, docDict, 'cc=', memberDefs);
559 TKR_addACItemList(qeWords, docDict, 'cc=-', memberDefs);
560 TKR_addACItemList(qeWords, docDict, 'status=', openDefs);
561 TKR_addACItemList(qeWords, docDict, 'status=', closedDefs);
562 TKR_addACItem(qeWords, docDict, 'summary=""', 'Set the summary field');
563
564 TKR_quickEditStore = new _AC_SimpleStore(qeWords, docDict);
565
566 // When we insert an autocomplete value, replace an entire command part.
567 // Add just a space after it (not a comma) if it is a complete part,
568 // or leave the caret immediately after the completion if we are just helping
569 // the user with the command operator.
570 TKR_quickEditStore.substitute =
571 function(inputValue, caret, completable, completion) {
572 let nextTerm = caret;
573 while (inputValue.charAt(nextTerm) != ' ' &&
574 nextTerm < inputValue.length) {
575 nextTerm++;
576 }
577 while (inputValue.charAt(nextTerm) == ' ' &&
578 nextTerm < inputValue.length) {
579 nextTerm++;
580 }
581 return inputValue.substring(0, caret - completable.length) +
582 completion.value + ' ' + inputValue.substring(nextTerm);
583 };
584}
585
586
587/**
588 * Constuct a new autocomplete store with all the project
589 * custom permissions.
590 * @param {Array} customPermissions An array of custom permission names.
591 */
592function TKR_setUpCustomPermissionsStore(customPermissions) {
593 customPermissions = customPermissions || [];
594 const permWords = ['View', 'EditIssue', 'AddIssueComment', 'DeleteIssue'];
595 const docdict = {
596 'View': '', 'EditIssue': '', 'AddIssueComment': '', 'DeleteIssue': ''};
597 for (let i = 0; i < customPermissions.length; i++) {
598 permWords.push(customPermissions[i]);
599 docdict[customPermissions[i]] = '';
600 }
601
602 TKR_customPermissionsStore = new _AC_SimpleStore(permWords, docdict);
603
604 TKR_customPermissionsStore.commaCompletes = false;
605
606 TKR_customPermissionsStore.substitute =
607 function(inputValue, cursor, completable, completion) {
608 return completion.value;
609 };
610}
611
612
613/**
614 * Constuct a new autocomplete store with all the well-known project
615 * member user names and real names. The store has some
616 * monorail-specific methods.
617 * TODO(jrobbins): would it be easier to define my own class to use
618 * instead of _AC_Simple_Store?
619 * @param {Array} memberDefs an array of member objects.
620 * @param {Array} nonGroupMemberDefs an array of member objects who are not groups.
621 */
622function TKR_setUpMemberStore(memberDefs, nonGroupMemberDefs) {
623 const memberWords = [];
624 const indMemberWords = [];
625 const docdict = {};
626
627 memberDefs.forEach((memberDef) => {
628 memberWords.push(memberDef.name);
629 docdict[memberDef.name] = null;
630 });
631 nonGroupMemberDefs.forEach((memberDef) => {
632 indMemberWords.push(memberDef.name);
633 });
634
635 TKR_memberListStore = new _AC_SimpleStore(memberWords, docdict);
636
637 TKR_memberListStore.completions = function(prefix, tofilter) {
638 const fullList = TKR_fullComplete(prefix, memberDefs);
639 if (fullList) return fullList;
640 return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
641 };
642
643 TKR_memberListStore.completable = function(inputValue, cursor) {
644 if (inputValue == '') return '*member';
645 return _AC_SimpleStore.prototype.completable.call(this, inputValue, cursor);
646 };
647
648 TKR_memberListStore.substitute = TKR_acSubstituteWithComma;
649
650 TKR_ownerStore = new _AC_SimpleStore(indMemberWords, docdict);
651
652 TKR_ownerStore.commaCompletes = false;
653
654 TKR_ownerStore.substitute =
655 function(inputValue, cursor, completable, completion) {
656 return completion.value;
657 };
658
659 TKR_ownerStore.completions = function(prefix, tofilter) {
660 const fullList = TKR_fullComplete(prefix, nonGroupMemberDefs);
661 if (fullList) return fullList;
662 return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
663 };
664
665 TKR_ownerStore.completable = function(inputValue, cursor) {
666 if (!ac_everTyped) return '*owner';
667 return inputValue;
668 };
669}
670
671
672/**
673 * Constuct one new autocomplete store for each user-valued custom
674 * field that has a needs_perm validation requirement, and thus a
675 * list of allowed user indexes.
676 * TODO(jrobbins): would it be easier to define my own class to use
677 * instead of _AC_Simple_Store?
678 * @param {Array} fieldDefs An array of field definitions, only some
679 * of which have a 'user_indexes' entry.
680 */
681function TKR_setUpUserAutocompleteStores(fieldDefs) {
682 fieldDefs.forEach((fieldDef) => {
683 if (fieldDef.qualifiedMembers) {
684 const us = makeOneUserAutocompleteStore(fieldDef);
685 TKR_userAutocompleteStores['custom_' + fieldDef['field_id']] = us;
686 }
687 });
688}
689
690function makeOneUserAutocompleteStore(fieldDef) {
691 const memberWords = [];
692 const docdict = {};
693 for (const member of fieldDef.qualifiedMembers) {
694 memberWords.push(member.name);
695 docdict[member.name] = member.doc;
696 }
697
698 const userStore = new _AC_SimpleStore(memberWords, docdict);
699 userStore.commaCompletes = false;
700
701 userStore.substitute =
702 function(inputValue, cursor, completable, completion) {
703 return completion.value;
704 };
705
706 userStore.completions = function(prefix, tofilter) {
707 const fullList = TKR_fullComplete(prefix, fieldDef.qualifiedMembers);
708 if (fullList) return fullList;
709 return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
710 };
711
712 userStore.completable = function(inputValue, cursor) {
713 if (!ac_everTyped) return '*custom';
714 return inputValue;
715 };
716
717 return userStore;
718}
719
720
721/**
722 * Constuct a new autocomplete store with all the components.
723 * The store has some monorail-specific methods.
724 * @param {Array} componentDefs An array of definitions of components.
725 */
726function TKR_setUpComponentStore(componentDefs) {
727 const componentWords = [];
728 const docdict = {};
729 for (let i = 0; i < componentDefs.length; i++) {
730 const component = componentDefs[i];
731 componentWords.push(component.name);
732 docdict[component.name] = component.doc;
733 }
734
735 const completions = function(prefix, tofilter) {
736 const fullList = TKR_fullComplete(prefix, componentDefs);
737 if (fullList) return fullList;
738 return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
739 };
740 const completable = function(inputValue, cursor) {
741 if (inputValue == '') return '*component';
742 return _AC_SimpleStore.prototype.completable.call(this, inputValue, cursor);
743 };
744
745 TKR_componentStore = new _AC_SimpleStore(componentWords, docdict);
746 TKR_componentStore.commaCompletes = false;
747 TKR_componentStore.substitute =
748 function(inputValue, cursor, completable, completion) {
749 return completion.value;
750 };
751 TKR_componentStore.completions = completions;
752 TKR_componentStore.completable = completable;
753
754 TKR_componentListStore = new _AC_SimpleStore(componentWords, docdict);
755 TKR_componentListStore.commaCompletes = false;
756 TKR_componentListStore.substitute = TKR_acSubstituteWithComma;
757 TKR_componentListStore.completions = completions;
758 TKR_componentListStore.completable = completable;
759}
760
761
762/**
763 * An array of definitions of all well-known issue labels. Each
764 * definition has the name of the label, and a docstring that
765 * describes its meaning.
766 */
767let TKR_labelWords = [];
768
769
770/**
771 * Constuct a new autocomplete store with all the well-known issue
772 * labels for the current project. The store has some DIT-specific methods.
773 * TODO(jrobbins): would it be easier to define my own class to use
774 * instead of _AC_Simple_Store?
775 * @param {Array} labelDefs An array of definitions of the project
776 * members. Each definition has a name and docstring.
777 */
778function TKR_setUpLabelStore(labelDefs) {
779 TKR_labelWords = [];
780 const TKR_labelPrefixes = [];
781 const labelPrefs = new Set();
782 const docdict = {};
783 for (let i = 0; i < labelDefs.length; i++) {
784 const label = labelDefs[i];
785 TKR_labelWords.push(label.name);
786 TKR_labelPrefixes.push(label.name.split('-')[0]);
787 docdict[label.name] = label.doc;
788 labelPrefs.add(label.name.split('-')[0]);
789 }
790 const labelPrefArray = Array.from(labelPrefs);
791 const labelPrefDefs = labelPrefArray.map((s) => ({name: s, doc: ''}));
792
793 TKR_labelStore = new _AC_SimpleStore(TKR_labelWords, docdict);
794
795 TKR_labelStore.commaCompletes = false;
796 TKR_labelStore.substitute =
797 function(inputValue, cursor, completable, completion) {
798 return completion.value;
799 };
800
801 TKR_labelPrefixStore = new _AC_SimpleStore(TKR_labelPrefixes);
802
803 TKR_labelPrefixStore.commaCompletes = false;
804 TKR_labelPrefixStore.substitute =
805 function(inputValue, cursor, completable, completion) {
806 return completion.value;
807 };
808
809 TKR_labelMultiStore = new _AC_SimpleStore(TKR_labelWords, docdict);
810
811 TKR_labelMultiStore.substitute = TKR_acSubstituteWithComma;
812
813 const completable = function(inputValue, cursor) {
814 if (cursor === 0) {
815 return '*label'; // Show every well-known label that is not redundant.
816 }
817 let start = 0;
818 for (let i = cursor; --i >= 0;) {
819 const c = inputValue.charAt(i);
820 if (c === ' ' || c === ',') {
821 start = i + 1;
822 break;
823 }
824 }
825 const questionPos = inputValue.indexOf('?');
826 if (questionPos >= 0) {
827 // Ignore any "?" character and anything after it.
828 inputValue = inputValue.substring(start, questionPos);
829 }
830 let result = inputValue.substring(start, cursor);
831 if (inputValue.lastIndexOf('-') > 0 && !ac_everTyped) {
832 // Act like a menu: offer all alternative values for the same prefix.
833 result = inputValue.substring(
834 start, Math.min(cursor, inputValue.lastIndexOf('-')));
835 }
836 if (inputValue.startsWith('Restrict-') && !ac_everTyped) {
837 // If user is in the middle of 2nd part, use that to narrow the choices.
838 result = inputValue;
839 // If they completed 2nd part, give all choices matching 2-part prefix.
840 if (inputValue.lastIndexOf('-') > 8) {
841 result = inputValue.substring(
842 start, Math.min(cursor, inputValue.lastIndexOf('-') + 1));
843 }
844 }
845
846 return result;
847 };
848
849 const computeAvoid = function() {
850 const labelTextFields = Array.from(
851 document.querySelectorAll('.labelinput'));
852 const otherTextFields = labelTextFields.filter(
853 (tf) => (tf !== ac_focusedInput && tf.value));
854 return otherTextFields.map((tf) => tf.value);
855 };
856
857
858 const completions = function(labeldic) {
859 return function(prefix, tofilter) {
860 let comps = TKR_fullComplete(prefix, labeldic);
861 if (comps === null) {
862 comps = _AC_SimpleStore.prototype.completions.call(
863 this, prefix, tofilter);
864 }
865
866 const filteredComps = [];
867 for (const completion of comps) {
868 const completionLower = completion.value.toLowerCase();
869 const labelPrefix = completionLower.split('-')[0];
870 let alreadyUsed = false;
871 const isExclusive = FindInArray(TKR_exclPrefixes, labelPrefix) !== -1;
872 if (isExclusive) {
873 for (const usedLabel of ac_avoidValues) {
874 if (usedLabel.startsWith(labelPrefix + '-')) {
875 alreadyUsed = true;
876 break;
877 }
878 }
879 }
880 if (!alreadyUsed) {
881 filteredComps.push(completion);
882 }
883 }
884
885 return filteredComps;
886 };
887 };
888
889 TKR_labelStore.computeAvoid = computeAvoid;
890 TKR_labelStore.completable = completable;
891 TKR_labelStore.completions = completions(labelDefs);
892
893 TKR_labelPrefixStore.completable = completable;
894 TKR_labelPrefixStore.completions = completions(labelPrefDefs);
895
896 TKR_labelMultiStore.completable = completable;
897 TKR_labelMultiStore.completions = completions(labelDefs);
898}
899
900
901/**
902 * Constuct a new autocomplete store with the given strings as choices.
903 * @param {Array} choices An array of autocomplete choices.
904 */
905function TKR_setUpAutoCompleteStore(choices) {
906 TKR_autoCompleteStore = new _AC_SimpleStore(choices);
907 const choicesDefs = [];
908 for (let i = 0; i < choices.length; ++i) {
909 choicesDefs.push({'name': choices[i], 'doc': ''});
910 }
911
912 /**
913 * Override the default completions() function to return a list of
914 * available choices. It proactively shows all choices when the user has
915 * not yet typed anything. It stops offering choices if the text field
916 * has a pretty long string in it already. It does not offer choices that
917 * have already been chosen.
918 */
919 TKR_autoCompleteStore.completions = function(prefix, tofilter) {
920 if (prefix.length > 18) {
921 return [];
922 }
923 let comps = TKR_fullComplete(prefix, choicesDefs);
924 if (comps == null) {
925 comps = _AC_SimpleStore.prototype.completions.call(
926 this, prefix, tofilter);
927 }
928
929 const usedComps = {};
930 const textFields = document.getElementsByTagName('input');
931 for (var i = 0; i < textFields.length; ++i) {
932 if (textFields[i].classList.contains('autocomplete')) {
933 usedComps[textFields[i].value] = true;
934 }
935 }
936 const unusedComps = [];
937 for (i = 0; i < comps.length; ++i) {
938 if (!usedComps[comps[i].value]) {
939 unusedComps.push(comps[i]);
940 }
941 }
942
943 return unusedComps;
944 };
945
946 /**
947 * Override the default completable() function with one that gives a
948 * special value when the user has not yet typed anything. This
949 * causes TKR_fullComplete() to show all choices. Also, always consider
950 * the whole textfield value as an input to completion matching. Otherwise,
951 * it would only consider the part after the last comma (which makes sense
952 * for gmail To: and Cc: address fields).
953 */
954 TKR_autoCompleteStore.completable = function(inputValue, cursor) {
955 if (inputValue == '') {
956 return '*ac';
957 }
958 return inputValue;
959 };
960
961 /**
962 * Override the default substitute() function to completely replace the
963 * contents of the text field when the user selects a completion. Otherwise,
964 * it would append, much like the Gmail To: and Cc: fields append autocomplete
965 * selections.
966 */
967 TKR_autoCompleteStore.substitute =
968 function(inputValue, cursor, completable, completion) {
969 return completion.value;
970 };
971
972 /**
973 * We consider the whole textfield to be one value, not a comma separated
974 * list. So, typing a ',' should not trigger an autocomplete selection.
975 */
976 TKR_autoCompleteStore.commaCompletes = false;
977}
978
979
980/**
981 * XMLHTTP object used to fetch autocomplete options from the server.
982 */
983const TKR_optionsXmlHttp = undefined;
984
985/**
986 * Contact the server to fetch the set of autocomplete options for the
987 * projects the user is contributor/member/owner of.
988 * @param {multiValue} boolean If set to true, the projectStore is configured to
989 * have support for multi-values (useful for example for saved queries where
990 * a query can apply to multiple projects).
991 */
992function TKR_fetchUserProjects(multiValue) {
993 // Set a request token to prevent XSRF leaking of user project lists.
994 const userRefs = [{displayName: window.CS_env.loggedInUserEmail}];
995 const userProjectsPromise = window.prpcClient.call(
996 'monorail.Users', 'GetUsersProjects', {userRefs});
997 userProjectsPromise.then((response) => {
998 const userProjects = response.usersProjects[0];
999 const projects = (userProjects.ownerOf || [])
1000 .concat(userProjects.memberOf || [])
1001 .concat(userProjects.contributorTo || []);
1002 projects.sort();
1003 if (projects) {
1004 TKR_setUpProjectStore(projects, multiValue);
1005 }
1006 });
1007}
1008
1009
1010/**
1011 * Constuct a new autocomplete store with all the projects that the
1012 * current user has visibility into. The store has some monorail-specific
1013 * methods.
1014 * @param {Array} projects An array of project names.
1015 * @param {boolean} multiValue Determines whether the store should support
1016 * multiple values.
1017 */
1018function TKR_setUpProjectStore(projects, multiValue) {
1019 const projectsDefs = [];
1020 const docdict = {};
1021 for (let i = 0; i < projects.length; ++i) {
1022 projectsDefs.push({'name': projects[i], 'doc': ''});
1023 docdict[projects[i]] = '';
1024 }
1025
1026 TKR_projectStore = new _AC_SimpleStore(projects, docdict);
1027 TKR_projectStore.commaCompletes = !multiValue;
1028
1029 if (multiValue) {
1030 TKR_projectStore.substitute = TKR_acSubstituteWithComma;
1031 } else {
1032 TKR_projectStore.substitute =
1033 function(inputValue, cursor, completable, completion) {
1034 return completion.value;
1035 };
1036 }
1037
1038 TKR_projectStore.completions = function(prefix, tofilter) {
1039 const fullList = TKR_fullComplete(prefix, projectsDefs);
1040 if (fullList) return fullList;
1041 return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
1042 };
1043
1044 TKR_projectStore.completable = function(inputValue, cursor) {
1045 if (inputValue == '') return '*project';
1046 if (multiValue) {
1047 return _AC_SimpleStore.prototype.completable.call(
1048 this, inputValue, cursor);
1049 } else {
1050 return inputValue;
1051 }
1052 };
1053}
1054
1055
1056/**
1057 * Convert the object resulting of a monorail.Projects ListStatuses to
1058 * the format expected by TKR_populateAutocomplete.
1059 * @param {object} statusesResponse A pRPC ListStatusesResponse object.
1060 */
1061function TKR_convertStatuses(statusesResponse) {
1062 const statusDefs = statusesResponse.statusDefs || [];
1063 const jsonData = {};
1064
1065 // Split statusDefs into open and closed name-doc objects.
1066 jsonData.open = [];
1067 jsonData.closed = [];
1068 for (const s of statusDefs) {
1069 if (!s.deprecated) {
1070 const item = {
1071 name: s.status,
1072 doc: s.docstring,
1073 };
1074 if (s.meansOpen) {
1075 jsonData.open.push(item);
1076 } else {
1077 jsonData.closed.push(item);
1078 }
1079 }
1080 }
1081
1082 jsonData.strict = statusesResponse.restrictToKnown;
1083
1084 return jsonData;
1085}
1086
1087
1088/**
1089 * Convert the object resulting of a monorail.Projects ListComponents to
1090 * the format expected by TKR_populateAutocomplete.
1091 * @param {object} componentsResponse A pRPC ListComponentsResponse object.
1092 */
1093function TKR_convertComponents(componentsResponse) {
1094 const componentDefs = (componentsResponse.componentDefs || []);
1095 const jsonData = {};
1096
1097 // Filter out deprecated components and normalize to name-doc object.
1098 jsonData.components = [];
1099 for (const c of componentDefs) {
1100 if (!c.deprecated) {
1101 jsonData.components.push({
1102 name: c.path,
1103 doc: c.docstring,
1104 });
1105 }
1106 }
1107
1108 return jsonData;
1109}
1110
1111
1112/**
1113 * Convert the object resulting of a monorail.Projects GetLabelOptions
1114 * call to the format expected by TKR_populateAutocomplete.
1115 * @param {object} labelsResponse A pRPC GetLabelOptionsResponse.
1116 * @param {Array<FieldDef>=} fieldDefs FieldDefs from a project config, used to
1117 * mask labels that are used to implement custom enum fields.
1118 */
1119function TKR_convertLabels(labelsResponse, fieldDefs = []) {
1120 const labelDefs = (labelsResponse.labelDefs || []);
1121 const exclusiveLabelPrefixes = (labelsResponse.exclusiveLabelPrefixes || []);
1122 const jsonData = {};
1123
1124 const maskedLabels = new Set();
1125 fieldDefs.forEach((fd) => {
1126 if (fd.enumChoices) {
1127 fd.enumChoices.forEach(({label}) => {
1128 maskedLabels.add(`${fd.fieldRef.fieldName}-${label}`);
1129 });
1130 }
1131 });
1132
1133 jsonData.labels = labelDefs.filter(({label}) => !maskedLabels.has(label)).map(
1134 (label) => ({name: label.label, doc: label.docstring}));
1135
1136 jsonData.excl_prefixes = exclusiveLabelPrefixes.map(
1137 (prefix) => prefix.toLowerCase());
1138
1139 return jsonData;
1140}
1141
1142
1143/**
1144 * Convert the object resulting of a monorail.Projects GetVisibleMembers
1145 * call to the format expected by TKR_populateAutocomplete.
1146 * @param {object?} visibleMembersResponse A pRPC GetVisibleMembersResponse.
1147 * @return {{memberEmails: {name: string}, nonGroupEmails: {name: string}}}
1148 */
1149function TKR_convertVisibleMembers(visibleMembersResponse) {
1150 if (!visibleMembersResponse) {
1151 visibleMembersResponse = {};
1152 }
1153 const groupRefs = (visibleMembersResponse.groupRefs || []);
1154 const userRefs = (visibleMembersResponse.userRefs || []);
1155 const jsonData = {};
1156
1157 const groupEmails = new Set(groupRefs.map(
1158 (groupRef) => groupRef.displayName));
1159
1160 jsonData.memberEmails = userRefs.map(
1161 (userRef) => ({name: userRef.displayName}));
1162 jsonData.nonGroupEmails = jsonData.memberEmails.filter(
1163 (memberEmail) => !groupEmails.has(memberEmail));
1164
1165 return jsonData;
1166}
1167
1168
1169/**
1170 * Convert the object resulting of a monorail.Projects ListFields to
1171 * the format expected by TKR_populateAutocomplete.
1172 * @param {object} fieldsResponse A pRPC ListFieldsResponse object.
1173 */
1174function TKR_convertFields(fieldsResponse) {
1175 const fieldDefs = (fieldsResponse.fieldDefs || []);
1176 const jsonData = {};
1177
1178 jsonData.fields = fieldDefs.map((field) =>
1179 ({
1180 field_id: field.fieldRef.fieldId,
1181 field_name: field.fieldRef.fieldName,
1182 field_type: field.fieldRef.type,
1183 docstring: field.docstring,
1184 choices: (field.enumChoices || []).map(
1185 (choice) => ({name: choice.label, doc: choice.docstring})),
1186 qualifiedMembers: (field.userChoices || []).map(
1187 (userRef) => ({name: userRef.displayName})),
1188 }),
1189 );
1190
1191 return jsonData;
1192}
1193
1194
1195/**
1196 * Convert the object resulting of a monorail.Features ListHotlistsByUser
1197 * call to the format expected by TKR_populateAutocomplete.
1198 * @param {Array<HotlistV0>} hotlists A lists of hotlists
1199 * @return {Array<{ref_str: string, summary: string}>}
1200 */
1201function TKR_convertHotlists(hotlists) {
1202 if (hotlists === undefined) {
1203 return [];
1204 }
1205
1206 const seen = new Set();
1207 const ambiguousNames = new Set();
1208
1209 hotlists.forEach((hotlist) => {
1210 if (seen.has(hotlist.name)) {
1211 ambiguousNames.add(hotlist.name);
1212 }
1213 seen.add(hotlist.name);
1214 });
1215
1216 return hotlists.map((hotlist) => {
1217 let ref_str = hotlist.name;
1218 if (ambiguousNames.has(hotlist.name)) {
1219 ref_str = hotlist.owner_ref.display_name + ':' + ref_str;
1220 }
1221 return {ref_str: ref_str, summary: hotlist.summary};
1222 });
1223}
1224
1225
1226/**
1227 * Initializes hotlists in autocomplete store.
1228 * @param {Array<HotlistV0>} hotlists
1229 */
1230function TKR_populateHotlistAutocomplete(hotlists) {
1231 TKR_setUpHotlistsStore(TKR_convertHotlists(hotlists));
1232}
1233
1234
1235/**
1236 * Add project config data that's already been fetched to the legacy
1237 * autocomplete.
1238 * @param {Config} projectConfig Returned projectConfig data.
1239 * @param {GetVisibleMembersResponse} visibleMembers
1240 * @param {Array<string>} customPermissions
1241 */
1242function TKR_populateAutocomplete(projectConfig, visibleMembers,
1243 customPermissions = []) {
1244 const {statusDefs, componentDefs, labelDefs, fieldDefs,
1245 exclusiveLabelPrefixes, projectName} = projectConfig;
1246
1247 const {memberEmails, nonGroupEmails} =
1248 TKR_convertVisibleMembers(visibleMembers);
1249 TKR_setUpMemberStore(memberEmails, nonGroupEmails);
1250 TKR_prepOwnerField(memberEmails);
1251
1252 const {open, closed, strict} = TKR_convertStatuses({statusDefs});
1253 TKR_setUpStatusStore(open, closed);
1254 TKR_restrict_to_known = strict;
1255
1256 const {components} = TKR_convertComponents({componentDefs});
1257 TKR_setUpComponentStore(components);
1258
1259 const {excl_prefixes, labels} = TKR_convertLabels(
1260 {labelDefs, exclusiveLabelPrefixes}, fieldDefs);
1261 TKR_exclPrefixes = excl_prefixes;
1262 TKR_setUpLabelStore(labels);
1263
1264 const {fields} = TKR_convertFields({fieldDefs});
1265 TKR_setUpUserAutocompleteStores(fields);
1266
1267 /* QuickEdit is not yet in Monorail. crbug.com/monorail/1926
1268 TKR_setUpQuickEditStore(
1269 jsonData.labels, jsonData.memberEmails, jsonData.open, jsonData.closed,
1270 jsonData.nonGroupEmails);
1271 */
1272
1273 // We need to wait until both exclusive prefixes (in configPromise) and
1274 // labels (in labelsPromise) have been read.
1275 TKR_prepLabelAC(TKR_labelFieldIDPrefix);
1276
1277 TKR_setUpSearchStore(
1278 labels, memberEmails, open, closed,
1279 components, fields, nonGroupEmails);
1280
1281 TKR_setUpCustomPermissionsStore(customPermissions);
1282}