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