blob: bf898ce48b62d1ffdcee4ec2604321a5a2d9fd4c [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable camelcase */
/* eslint-disable no-unused-vars */
/**
* This file contains the autocomplete configuration logic that is
* specific to the issue fields of Monorail. It depends on ac.js, our
* modified version of the autocomplete library.
*/
/**
* This is an autocomplete store that holds the hotlists of the current user.
*/
let TKR_hotlistsStore;
/**
* This is an autocomplete store that holds well-known issue label
* values for the current project.
*/
let TKR_labelStore;
/**
* Like TKR_labelStore but stores only label prefixes.
*/
let TKR_labelPrefixStore;
/**
* Like TKR_labelStore but adds a trailing comma instead of replacing.
*/
let TKR_labelMultiStore;
/**
* This is an autocomplete store that holds issue components.
*/
let TKR_componentStore;
/**
* Like TKR_componentStore but adds a trailing comma instead of replacing.
*/
let TKR_componentListStore;
/**
* This is an autocomplete store that holds many different kinds of
* items that can be shown in the artifact search autocomplete.
*/
let TKR_searchStore;
/**
* This is similar to TKR_searchStore, but does not include any suggestions
* to use the "me" keyword. Using "me" is not a good idea for project canned
* queries and filter rules.
*/
let TKR_projectQueryStore;
/**
* This is an autocomplete store that holds items for the quick edit
* autocomplete.
*/
// TODO(jrobbins): add options for fields and components.
let TKR_quickEditStore;
/**
* This is a list of label prefixes that each issue should only use once.
* E.g., each issue should only have one Priority-* label. We do not prevent
* the user from using multiple such labels, we just warn the user before
* they submit.
*/
let TKR_exclPrefixes = [];
/**
* This is an autocomplete store that holds custom permission names that
* have already been used in this project.
*/
let TKR_customPermissionsStore;
/**
* This is an autocomplete store that holds well-known issue status
* values for the current project.
*/
let TKR_statusStore;
/**
* This is an autocomplete store that holds the usernames of all the
* members of the current project. This is used for autocomplete in
* the cc-list of an issue, where many user names can entered with
* commas between them.
*/
let TKR_memberListStore;
/**
* This is an autocomplete store that holds the projects that the current
* user is contributor/member/owner of.
*/
let TKR_projectStore;
/**
* This is an autocomplete store that holds the usernames of possible
* issue owners in the current project. The list of possible issue
* owners is the same as the list of project members, but the behavior
* of this autocompete store is different because the issue owner text
* field can only accept one value.
*/
let TKR_ownerStore;
/**
* This is an autocomplete store that holds any list of string for choices.
*/
let TKR_autoCompleteStore;
/**
* An array of autocomplete stores used for user-type custom fields.
*/
const TKR_userAutocompleteStores = [];
/**
* This boolean controls whether odd-ball status and labels are treated as
* a warning or an error. Normally, it is False.
*/
// TODO(jrobbins): split this into one option for statuses and one for labels.
let TKR_restrict_to_known;
/**
* This substitute function should be used for multi-valued autocomplete fields
* that are delimited by commas. When we insert an autocomplete value, replace
* an entire search term. Add a comma and a space after it if it is a complete
* search term.
*/
function TKR_acSubstituteWithComma(inputValue, caret, completable, completion) {
let nextTerm = caret;
// Subtract one in case the cursor is at the end of the input, before a comma.
let prevTerm = caret - 1;
while (nextTerm < inputValue.length - 1 && inputValue.charAt(nextTerm) !== ',') {
nextTerm++;
}
// Set this at the position after the found comma.
nextTerm++;
while (prevTerm > 0 && ![',', ' '].includes(inputValue.charAt(prevTerm))) {
prevTerm--;
}
if (prevTerm > 0) {
// Set this boundary after the found space/comma if it's not the beginning
// of the field.
prevTerm++;
}
return inputValue.substring(0, prevTerm) +
completion.value + ', ' + inputValue.substring(nextTerm);
}
/**
* When the prefix starts with '*', return the complete set of all
* possible completions.
* @param {string} prefix If this starts with '*', return all possible
* completions. Otherwise return null.
* @param {Array} labelDefs The array of label names and docstrings.
* @return Array of new _AC_Completions for each possible completion, or null.
*/
function TKR_fullComplete(prefix, labelDefs) {
if (!prefix.startsWith('*')) return null;
const out = [];
for (let i = 0; i < labelDefs.length; i++) {
out.push(new _AC_Completion(labelDefs[i].name,
labelDefs[i].name,
labelDefs[i].doc));
}
return out;
}
/**
* Constucts a list of all completions for both open and closed
* statuses, with a header for each group.
* @param {string} prefix If starts with '*', return all possible completions,
* else return null.
* @param {Array} openStatusDefs The array of open status values and
* docstrings.
* @param {Array} closedStatusDefs The array of closed status values
* and docstrings.
* @return Array of new _AC_Completions for each possible completion, or null.
*/
function TKR_openClosedComplete(prefix, openStatusDefs, closedStatusDefs) {
if (!prefix.startsWith('*')) return null;
const out = [];
out.push({heading: 'Open Statuses:'}); // TODO: i18n
for (var i = 0; i < openStatusDefs.length; i++) {
out.push(new _AC_Completion(openStatusDefs[i].name,
openStatusDefs[i].name,
openStatusDefs[i].doc));
}
out.push({heading: 'Closed Statuses:'}); // TODO: i18n
for (var i = 0; i < closedStatusDefs.length; i++) {
out.push(new _AC_Completion(closedStatusDefs[i].name,
closedStatusDefs[i].name,
closedStatusDefs[i].doc));
}
return out;
}
function TKR_setUpHotlistsStore(hotlists) {
const docdict = {};
const ref_strs = [];
for (let i = 0; i < hotlists.length; i++) {
ref_strs.push(hotlists[i]['ref_str']);
docdict[hotlists[i]['ref_str']] = hotlists[i]['summary'];
}
TKR_hotlistsStore = new _AC_SimpleStore(ref_strs, docdict);
TKR_hotlistsStore.substitute = TKR_acSubstituteWithComma;
}
/**
* An array of definitions of all well-known issue statuses. Each
* definition has the name of the status value, and a docstring that
* describes its meaning.
*/
let TKR_statusWords = [];
/**
* Constuct a new autocomplete store with all the well-known issue
* status values. The store has some DIT-specific methods.
* TODO(jrobbins): would it be easier to define my own class to use
* instead of _AC_Simple_Store?
* @param {Array} openStatusDefs An array of definitions of the
* well-known open status values. Each definition has a name and
* docstring.
* @param {Array} closedStatusDefs An array of definitions of the
* well-known closed status values. Each definition has a name and
* docstring.
*/
function TKR_setUpStatusStore(openStatusDefs, closedStatusDefs) {
const docdict = {};
TKR_statusWords = [];
for (var i = 0; i < openStatusDefs.length; i++) {
var status = openStatusDefs[i];
TKR_statusWords.push(status.name);
docdict[status.name] = status.doc;
}
for (var i = 0; i < closedStatusDefs.length; i++) {
var status = closedStatusDefs[i];
TKR_statusWords.push(status.name);
docdict[status.name] = status.doc;
}
TKR_statusStore = new _AC_SimpleStore(TKR_statusWords, docdict);
TKR_statusStore.commaCompletes = false;
TKR_statusStore.substitute =
function(inputValue, cursor, completable, completion) {
return completion.value;
};
TKR_statusStore.completable = function(inputValue, cursor) {
if (!ac_everTyped) return '*status';
return inputValue;
};
TKR_statusStore.completions = function(prefix, tofilter) {
const fullList = TKR_openClosedComplete(prefix,
openStatusDefs,
closedStatusDefs);
if (fullList) return fullList;
return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
};
}
/**
* Simple function to add a given item to the list of items used to construct
* an "autocomplete store", and also update the docstring that describes
* that item. They are stored separately for backward compatability with
* autocomplete store logic that preceeded the introduction of descriptions.
*/
function TKR_addACItem(items, docDict, item, docStr) {
items.push(item);
docDict[item] = docStr;
}
/**
* Adds a group of three items related to a date field.
*/
function TKR_addACDateItems(items, docDict, fieldName, humanReadable) {
const today = new Date();
const todayStr = (today.getFullYear() + '-' + (today.getMonth() + 1) + '-' +
today.getDate());
TKR_addACItem(items, docDict, fieldName + '>today-1',
humanReadable + ' within the last N days');
TKR_addACItem(items, docDict, fieldName + '>' + todayStr,
humanReadable + ' after the specified date');
TKR_addACItem(items, docDict, fieldName + '<today-1',
humanReadable + ' more than N days ago');
}
/**
* Add several autocomplete items to a word list that will be used to construct
* an autocomplete store. Also, keep track of description strings for each
* item. A search operator is prepended to the name of each item. The opt_old
* and opt_new parameters are used to transform Key-Value labels into Key=Value
* search terms.
*/
function TKR_addACItemList(
items, docDict, searchOp, acDefs, opt_old, opt_new) {
let item;
for (let i = 0; i < acDefs.length; i++) {
const nameAndDoc = acDefs[i];
item = searchOp + nameAndDoc.name;
if (opt_old) {
// Preserve any leading minus-sign.
item = item.slice(0, 1) + item.slice(1).replace(opt_old, opt_new);
}
TKR_addACItem(items, docDict, item, nameAndDoc.doc);
}
}
/**
* Use information from an options feed to populate the artifact search
* autocomplete menu. The order of sections is: custom fields, labels,
* components, people, status, special, dates. Within each section,
* options are ordered semantically where possible, or alphabetically
* if there is no semantic ordering. Negated options all come after
* all normal options.
*/
function TKR_setUpSearchStore(
labelDefs, memberDefs, openDefs, closedDefs, componentDefs, fieldDefs,
indMemberDefs) {
let searchWords = [];
const searchWordsNeg = [];
const docDict = {};
// Treat Key-Value and OneWord labels separately.
const keyValueLabelDefs = [];
const oneWordLabelDefs = [];
for (var i = 0; i < labelDefs.length; i++) {
const nameAndDoc = labelDefs[i];
if (nameAndDoc.name.indexOf('-') == -1) {
oneWordLabelDefs.push(nameAndDoc);
} else {
keyValueLabelDefs.push(nameAndDoc);
}
}
// Autocomplete for custom fields.
for (i = 0; i < fieldDefs.length; i++) {
const fieldName = fieldDefs[i]['field_name'];
const fieldType = fieldDefs[i]['field_type'];
if (fieldType == 'ENUM_TYPE') {
const choices = fieldDefs[i]['choices'];
TKR_addACItemList(searchWords, docDict, fieldName + '=', choices);
TKR_addACItemList(searchWordsNeg, docDict, '-' + fieldName + '=', choices);
} else if (fieldType == 'STR_TYPE') {
TKR_addACItem(searchWords, docDict, fieldName + ':',
fieldDefs[i]['docstring']);
} else if (fieldType == 'DATE_TYPE') {
TKR_addACItem(searchWords, docDict, fieldName + ':',
fieldDefs[i]['docstring']);
TKR_addACDateItems(searchWords, docDict, fieldName, fieldName);
} else {
TKR_addACItem(searchWords, docDict, fieldName + '=',
fieldDefs[i]['docstring']);
}
TKR_addACItem(searchWords, docDict, 'has:' + fieldName,
'Issues with any ' + fieldName + ' value');
TKR_addACItem(searchWordsNeg, docDict, '-has:' + fieldName,
'Issues with no ' + fieldName + ' value');
}
// Add suggestions with "me" first, because otherwise they may be impossible
// to reach in a project that has a lot of members with emails starting with
// "me".
if (CS_env['loggedInUserEmail']) {
TKR_addACItem(searchWords, docDict, 'owner:me', 'Issues owned by me');
TKR_addACItem(searchWordsNeg, docDict, '-owner:me', 'Issues not owned by me');
TKR_addACItem(searchWords, docDict, 'cc:me', 'Issues that CC me');
TKR_addACItem(searchWordsNeg, docDict, '-cc:me', 'Issues that don\'t CC me');
TKR_addACItem(searchWords, docDict, 'reporter:me', 'Issues I reported');
TKR_addACItem(searchWordsNeg, docDict, '-reporter:me', 'Issues reported by others');
TKR_addACItem(searchWords, docDict, 'commentby:me',
'Issues that I commented on');
TKR_addACItem(searchWordsNeg, docDict, '-commentby:me',
'Issues that I didn\'t comment on');
}
TKR_addACItemList(searchWords, docDict, '', keyValueLabelDefs, '-', '=');
TKR_addACItemList(searchWordsNeg, docDict, '-', keyValueLabelDefs, '-', '=');
TKR_addACItemList(searchWords, docDict, 'label:', oneWordLabelDefs);
TKR_addACItemList(searchWordsNeg, docDict, '-label:', oneWordLabelDefs);
TKR_addACItemList(searchWords, docDict, 'component:', componentDefs);
TKR_addACItemList(searchWordsNeg, docDict, '-component:', componentDefs);
TKR_addACItem(searchWords, docDict, 'has:component',
'Issues with any components specified');
TKR_addACItem(searchWordsNeg, docDict, '-has:component',
'Issues with no components specified');
TKR_addACItemList(searchWords, docDict, 'owner:', indMemberDefs);
TKR_addACItemList(searchWordsNeg, docDict, '-owner:', indMemberDefs);
TKR_addACItemList(searchWords, docDict, 'cc:', memberDefs);
TKR_addACItemList(searchWordsNeg, docDict, '-cc:', memberDefs);
TKR_addACItem(searchWords, docDict, 'has:cc',
'Issues with any cc\'d users');
TKR_addACItem(searchWordsNeg, docDict, '-has:cc',
'Issues with no cc\'d users');
TKR_addACItemList(searchWords, docDict, 'reporter:', memberDefs);
TKR_addACItemList(searchWordsNeg, docDict, '-reporter:', memberDefs);
TKR_addACItemList(searchWords, docDict, 'status:', openDefs);
TKR_addACItemList(searchWordsNeg, docDict, '-status:', openDefs);
TKR_addACItemList(searchWords, docDict, 'status:', closedDefs);
TKR_addACItemList(searchWordsNeg, docDict, '-status:', closedDefs);
TKR_addACItem(searchWords, docDict, 'has:status',
'Issues with any status');
TKR_addACItem(searchWordsNeg, docDict, '-has:status',
'Issues with no status');
TKR_addACItem(searchWords, docDict, 'is:blocked',
'Issues that are blocked');
TKR_addACItem(searchWordsNeg, docDict, '-is:blocked',
'Issues that are not blocked');
TKR_addACItem(searchWords, docDict, 'has:blockedon',
'Issues that are blocked');
TKR_addACItem(searchWordsNeg, docDict, '-has:blockedon',
'Issues that are not blocked');
TKR_addACItem(searchWords, docDict, 'has:blocking',
'Issues that are blocking other issues');
TKR_addACItem(searchWordsNeg, docDict, '-has:blocking',
'Issues that are not blocking other issues');
TKR_addACItem(searchWords, docDict, 'has:mergedinto',
'Issues that were merged into other issues');
TKR_addACItem(searchWordsNeg, docDict, '-has:mergedinto',
'Issues that were not merged into other issues');
TKR_addACItem(searchWords, docDict, 'is:starred',
'Starred by me');
TKR_addACItem(searchWordsNeg, docDict, '-is:starred',
'Not starred by me');
TKR_addACItem(searchWords, docDict, 'stars>10',
'More than 10 stars');
TKR_addACItem(searchWords, docDict, 'stars>100',
'More than 100 stars');
TKR_addACItem(searchWords, docDict, 'summary:',
'Search within the summary field');
TKR_addACItemList(searchWords, docDict, 'commentby:', memberDefs);
TKR_addACItem(searchWords, docDict, 'attachment:',
'Search within attachment names');
TKR_addACItem(searchWords, docDict, 'attachments>5',
'Has more than 5 attachments');
TKR_addACItem(searchWords, docDict, 'is:open', 'Issues that are open');
TKR_addACItem(searchWordsNeg, docDict, '-is:open', 'Issues that are closed');
TKR_addACItem(searchWords, docDict, 'has:owner',
'Issues with some owner');
TKR_addACItem(searchWordsNeg, docDict, '-has:owner',
'Issues with no owner');
TKR_addACItem(searchWords, docDict, 'has:attachments',
'Issues with some attachments');
TKR_addACItem(searchWords, docDict, 'id:1,2,3',
'Match only the specified issues');
TKR_addACItem(searchWords, docDict, 'id<100000',
'Issues with IDs under 100,000');
TKR_addACItem(searchWords, docDict, 'blockedon:1',
'Blocked on the specified issues');
TKR_addACItem(searchWords, docDict, 'blocking:1',
'Blocking the specified issues');
TKR_addACItem(searchWords, docDict, 'mergedinto:1',
'Merged into the specified issues');
TKR_addACItem(searchWords, docDict, 'is:ownerbouncing',
'Issues with owners we cannot contact');
TKR_addACItem(searchWords, docDict, 'is:spam', 'Issues classified as spam');
// We do not suggest -is:spam because it is implicit.
TKR_addACDateItems(searchWords, docDict, 'opened', 'Opened');
TKR_addACDateItems(searchWords, docDict, 'modified', 'Modified');
TKR_addACDateItems(searchWords, docDict, 'closed', 'Closed');
TKR_addACDateItems(searchWords, docDict, 'ownermodified', 'Owner field modified');
TKR_addACDateItems(searchWords, docDict, 'ownerlastvisit', 'Owner last visit');
TKR_addACDateItems(searchWords, docDict, 'statusmodified', 'Status field modified');
TKR_addACDateItems(
searchWords, docDict, 'componentmodified', 'Component field modified');
TKR_projectQueryStore = new _AC_SimpleStore(searchWords, docDict);
searchWords = searchWords.concat(searchWordsNeg);
TKR_searchStore = new _AC_SimpleStore(searchWords, docDict);
// When we insert an autocomplete value, replace an entire search term.
// Add just a space after it (not a comma) if it is a complete search term,
// or leave the caret immediately after the completion if we are just helping
// the user with the search operator.
TKR_searchStore.substitute =
function(inputValue, caret, completable, completion) {
let nextTerm = caret;
while (inputValue.charAt(nextTerm) != ' ' &&
nextTerm < inputValue.length) {
nextTerm++;
}
while (inputValue.charAt(nextTerm) == ' ' &&
nextTerm < inputValue.length) {
nextTerm++;
}
return inputValue.substring(0, caret - completable.length) +
completion.value + ' ' + inputValue.substring(nextTerm);
};
TKR_searchStore.autoselectFirstRow =
function() {
return false;
};
TKR_projectQueryStore.substitute = TKR_searchStore.substitute;
TKR_projectQueryStore.autoselectFirstRow = TKR_searchStore.autoselectFirstRow;
}
/**
* Use information from an options feed to populate the issue quick edit
* autocomplete menu.
*/
function TKR_setUpQuickEditStore(
labelDefs, memberDefs, openDefs, closedDefs, indMemberDefs) {
const qeWords = [];
const docDict = {};
// Treat Key-Value and OneWord labels separately.
const keyValueLabelDefs = [];
const oneWordLabelDefs = [];
for (let i = 0; i < labelDefs.length; i++) {
const nameAndDoc = labelDefs[i];
if (nameAndDoc.name.indexOf('-') == -1) {
oneWordLabelDefs.push(nameAndDoc);
} else {
keyValueLabelDefs.push(nameAndDoc);
}
}
TKR_addACItemList(qeWords, docDict, '', keyValueLabelDefs, '-', '=');
TKR_addACItemList(qeWords, docDict, '-', keyValueLabelDefs, '-', '=');
TKR_addACItemList(qeWords, docDict, '', oneWordLabelDefs);
TKR_addACItemList(qeWords, docDict, '-', oneWordLabelDefs);
TKR_addACItem(qeWords, docDict, 'owner=me', 'Make me the owner');
TKR_addACItem(qeWords, docDict, 'owner=----', 'Clear the owner field');
TKR_addACItem(qeWords, docDict, 'cc=me', 'CC me on this issue');
TKR_addACItem(qeWords, docDict, 'cc=-me', 'Remove me from CC list');
TKR_addACItemList(qeWords, docDict, 'owner=', indMemberDefs);
TKR_addACItemList(qeWords, docDict, 'cc=', memberDefs);
TKR_addACItemList(qeWords, docDict, 'cc=-', memberDefs);
TKR_addACItemList(qeWords, docDict, 'status=', openDefs);
TKR_addACItemList(qeWords, docDict, 'status=', closedDefs);
TKR_addACItem(qeWords, docDict, 'summary=""', 'Set the summary field');
TKR_quickEditStore = new _AC_SimpleStore(qeWords, docDict);
// When we insert an autocomplete value, replace an entire command part.
// Add just a space after it (not a comma) if it is a complete part,
// or leave the caret immediately after the completion if we are just helping
// the user with the command operator.
TKR_quickEditStore.substitute =
function(inputValue, caret, completable, completion) {
let nextTerm = caret;
while (inputValue.charAt(nextTerm) != ' ' &&
nextTerm < inputValue.length) {
nextTerm++;
}
while (inputValue.charAt(nextTerm) == ' ' &&
nextTerm < inputValue.length) {
nextTerm++;
}
return inputValue.substring(0, caret - completable.length) +
completion.value + ' ' + inputValue.substring(nextTerm);
};
}
/**
* Constuct a new autocomplete store with all the project
* custom permissions.
* @param {Array} customPermissions An array of custom permission names.
*/
function TKR_setUpCustomPermissionsStore(customPermissions) {
customPermissions = customPermissions || [];
const permWords = ['View', 'EditIssue', 'AddIssueComment', 'DeleteIssue'];
const docdict = {
'View': '', 'EditIssue': '', 'AddIssueComment': '', 'DeleteIssue': ''};
for (let i = 0; i < customPermissions.length; i++) {
permWords.push(customPermissions[i]);
docdict[customPermissions[i]] = '';
}
TKR_customPermissionsStore = new _AC_SimpleStore(permWords, docdict);
TKR_customPermissionsStore.commaCompletes = false;
TKR_customPermissionsStore.substitute =
function(inputValue, cursor, completable, completion) {
return completion.value;
};
}
/**
* Constuct a new autocomplete store with all the well-known project
* member user names and real names. The store has some
* monorail-specific methods.
* TODO(jrobbins): would it be easier to define my own class to use
* instead of _AC_Simple_Store?
* @param {Array} memberDefs an array of member objects.
* @param {Array} nonGroupMemberDefs an array of member objects who are not groups.
*/
function TKR_setUpMemberStore(memberDefs, nonGroupMemberDefs) {
const memberWords = [];
const indMemberWords = [];
const docdict = {};
memberDefs.forEach((memberDef) => {
memberWords.push(memberDef.name);
docdict[memberDef.name] = null;
});
nonGroupMemberDefs.forEach((memberDef) => {
indMemberWords.push(memberDef.name);
});
TKR_memberListStore = new _AC_SimpleStore(memberWords, docdict);
TKR_memberListStore.completions = function(prefix, tofilter) {
const fullList = TKR_fullComplete(prefix, memberDefs);
if (fullList) return fullList;
return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
};
TKR_memberListStore.completable = function(inputValue, cursor) {
if (inputValue == '') return '*member';
return _AC_SimpleStore.prototype.completable.call(this, inputValue, cursor);
};
TKR_memberListStore.substitute = TKR_acSubstituteWithComma;
TKR_ownerStore = new _AC_SimpleStore(indMemberWords, docdict);
TKR_ownerStore.commaCompletes = false;
TKR_ownerStore.substitute =
function(inputValue, cursor, completable, completion) {
return completion.value;
};
TKR_ownerStore.completions = function(prefix, tofilter) {
const fullList = TKR_fullComplete(prefix, nonGroupMemberDefs);
if (fullList) return fullList;
return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
};
TKR_ownerStore.completable = function(inputValue, cursor) {
if (!ac_everTyped) return '*owner';
return inputValue;
};
}
/**
* Constuct one new autocomplete store for each user-valued custom
* field that has a needs_perm validation requirement, and thus a
* list of allowed user indexes.
* TODO(jrobbins): would it be easier to define my own class to use
* instead of _AC_Simple_Store?
* @param {Array} fieldDefs An array of field definitions, only some
* of which have a 'user_indexes' entry.
*/
function TKR_setUpUserAutocompleteStores(fieldDefs) {
fieldDefs.forEach((fieldDef) => {
if (fieldDef.qualifiedMembers) {
const us = makeOneUserAutocompleteStore(fieldDef);
TKR_userAutocompleteStores['custom_' + fieldDef['field_id']] = us;
}
});
}
function makeOneUserAutocompleteStore(fieldDef) {
const memberWords = [];
const docdict = {};
for (const member of fieldDef.qualifiedMembers) {
memberWords.push(member.name);
docdict[member.name] = member.doc;
}
const userStore = new _AC_SimpleStore(memberWords, docdict);
userStore.commaCompletes = false;
userStore.substitute =
function(inputValue, cursor, completable, completion) {
return completion.value;
};
userStore.completions = function(prefix, tofilter) {
const fullList = TKR_fullComplete(prefix, fieldDef.qualifiedMembers);
if (fullList) return fullList;
return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
};
userStore.completable = function(inputValue, cursor) {
if (!ac_everTyped) return '*custom';
return inputValue;
};
return userStore;
}
/**
* Constuct a new autocomplete store with all the components.
* The store has some monorail-specific methods.
* @param {Array} componentDefs An array of definitions of components.
*/
function TKR_setUpComponentStore(componentDefs) {
const componentWords = [];
const docdict = {};
for (let i = 0; i < componentDefs.length; i++) {
const component = componentDefs[i];
componentWords.push(component.name);
docdict[component.name] = component.doc;
}
const completions = function(prefix, tofilter) {
const fullList = TKR_fullComplete(prefix, componentDefs);
if (fullList) return fullList;
return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
};
const completable = function(inputValue, cursor) {
if (inputValue == '') return '*component';
return _AC_SimpleStore.prototype.completable.call(this, inputValue, cursor);
};
TKR_componentStore = new _AC_SimpleStore(componentWords, docdict);
TKR_componentStore.commaCompletes = false;
TKR_componentStore.substitute =
function(inputValue, cursor, completable, completion) {
return completion.value;
};
TKR_componentStore.completions = completions;
TKR_componentStore.completable = completable;
TKR_componentListStore = new _AC_SimpleStore(componentWords, docdict);
TKR_componentListStore.commaCompletes = false;
TKR_componentListStore.substitute = TKR_acSubstituteWithComma;
TKR_componentListStore.completions = completions;
TKR_componentListStore.completable = completable;
}
/**
* An array of definitions of all well-known issue labels. Each
* definition has the name of the label, and a docstring that
* describes its meaning.
*/
let TKR_labelWords = [];
/**
* Constuct a new autocomplete store with all the well-known issue
* labels for the current project. The store has some DIT-specific methods.
* TODO(jrobbins): would it be easier to define my own class to use
* instead of _AC_Simple_Store?
* @param {Array} labelDefs An array of definitions of the project
* members. Each definition has a name and docstring.
*/
function TKR_setUpLabelStore(labelDefs) {
TKR_labelWords = [];
const TKR_labelPrefixes = [];
const labelPrefs = new Set();
const docdict = {};
for (let i = 0; i < labelDefs.length; i++) {
const label = labelDefs[i];
TKR_labelWords.push(label.name);
TKR_labelPrefixes.push(label.name.split('-')[0]);
docdict[label.name] = label.doc;
labelPrefs.add(label.name.split('-')[0]);
}
const labelPrefArray = Array.from(labelPrefs);
const labelPrefDefs = labelPrefArray.map((s) => ({name: s, doc: ''}));
TKR_labelStore = new _AC_SimpleStore(TKR_labelWords, docdict);
TKR_labelStore.commaCompletes = false;
TKR_labelStore.substitute =
function(inputValue, cursor, completable, completion) {
return completion.value;
};
TKR_labelPrefixStore = new _AC_SimpleStore(TKR_labelPrefixes);
TKR_labelPrefixStore.commaCompletes = false;
TKR_labelPrefixStore.substitute =
function(inputValue, cursor, completable, completion) {
return completion.value;
};
TKR_labelMultiStore = new _AC_SimpleStore(TKR_labelWords, docdict);
TKR_labelMultiStore.substitute = TKR_acSubstituteWithComma;
const completable = function(inputValue, cursor) {
if (cursor === 0) {
return '*label'; // Show every well-known label that is not redundant.
}
let start = 0;
for (let i = cursor; --i >= 0;) {
const c = inputValue.charAt(i);
if (c === ' ' || c === ',') {
start = i + 1;
break;
}
}
const questionPos = inputValue.indexOf('?');
if (questionPos >= 0) {
// Ignore any "?" character and anything after it.
inputValue = inputValue.substring(start, questionPos);
}
let result = inputValue.substring(start, cursor);
if (inputValue.lastIndexOf('-') > 0 && !ac_everTyped) {
// Act like a menu: offer all alternative values for the same prefix.
result = inputValue.substring(
start, Math.min(cursor, inputValue.lastIndexOf('-')));
}
if (inputValue.startsWith('Restrict-') && !ac_everTyped) {
// If user is in the middle of 2nd part, use that to narrow the choices.
result = inputValue;
// If they completed 2nd part, give all choices matching 2-part prefix.
if (inputValue.lastIndexOf('-') > 8) {
result = inputValue.substring(
start, Math.min(cursor, inputValue.lastIndexOf('-') + 1));
}
}
return result;
};
const computeAvoid = function() {
const labelTextFields = Array.from(
document.querySelectorAll('.labelinput'));
const otherTextFields = labelTextFields.filter(
(tf) => (tf !== ac_focusedInput && tf.value));
return otherTextFields.map((tf) => tf.value);
};
const completions = function(labeldic) {
return function(prefix, tofilter) {
let comps = TKR_fullComplete(prefix, labeldic);
if (comps === null) {
comps = _AC_SimpleStore.prototype.completions.call(
this, prefix, tofilter);
}
const filteredComps = [];
for (const completion of comps) {
const completionLower = completion.value.toLowerCase();
const labelPrefix = completionLower.split('-')[0];
let alreadyUsed = false;
const isExclusive = FindInArray(TKR_exclPrefixes, labelPrefix) !== -1;
if (isExclusive) {
for (const usedLabel of ac_avoidValues) {
if (usedLabel.startsWith(labelPrefix + '-')) {
alreadyUsed = true;
break;
}
}
}
if (!alreadyUsed) {
filteredComps.push(completion);
}
}
return filteredComps;
};
};
TKR_labelStore.computeAvoid = computeAvoid;
TKR_labelStore.completable = completable;
TKR_labelStore.completions = completions(labelDefs);
TKR_labelPrefixStore.completable = completable;
TKR_labelPrefixStore.completions = completions(labelPrefDefs);
TKR_labelMultiStore.completable = completable;
TKR_labelMultiStore.completions = completions(labelDefs);
}
/**
* Constuct a new autocomplete store with the given strings as choices.
* @param {Array} choices An array of autocomplete choices.
*/
function TKR_setUpAutoCompleteStore(choices) {
TKR_autoCompleteStore = new _AC_SimpleStore(choices);
const choicesDefs = [];
for (let i = 0; i < choices.length; ++i) {
choicesDefs.push({'name': choices[i], 'doc': ''});
}
/**
* Override the default completions() function to return a list of
* available choices. It proactively shows all choices when the user has
* not yet typed anything. It stops offering choices if the text field
* has a pretty long string in it already. It does not offer choices that
* have already been chosen.
*/
TKR_autoCompleteStore.completions = function(prefix, tofilter) {
if (prefix.length > 18) {
return [];
}
let comps = TKR_fullComplete(prefix, choicesDefs);
if (comps == null) {
comps = _AC_SimpleStore.prototype.completions.call(
this, prefix, tofilter);
}
const usedComps = {};
const textFields = document.getElementsByTagName('input');
for (var i = 0; i < textFields.length; ++i) {
if (textFields[i].classList.contains('autocomplete')) {
usedComps[textFields[i].value] = true;
}
}
const unusedComps = [];
for (i = 0; i < comps.length; ++i) {
if (!usedComps[comps[i].value]) {
unusedComps.push(comps[i]);
}
}
return unusedComps;
};
/**
* Override the default completable() function with one that gives a
* special value when the user has not yet typed anything. This
* causes TKR_fullComplete() to show all choices. Also, always consider
* the whole textfield value as an input to completion matching. Otherwise,
* it would only consider the part after the last comma (which makes sense
* for gmail To: and Cc: address fields).
*/
TKR_autoCompleteStore.completable = function(inputValue, cursor) {
if (inputValue == '') {
return '*ac';
}
return inputValue;
};
/**
* Override the default substitute() function to completely replace the
* contents of the text field when the user selects a completion. Otherwise,
* it would append, much like the Gmail To: and Cc: fields append autocomplete
* selections.
*/
TKR_autoCompleteStore.substitute =
function(inputValue, cursor, completable, completion) {
return completion.value;
};
/**
* We consider the whole textfield to be one value, not a comma separated
* list. So, typing a ',' should not trigger an autocomplete selection.
*/
TKR_autoCompleteStore.commaCompletes = false;
}
/**
* XMLHTTP object used to fetch autocomplete options from the server.
*/
const TKR_optionsXmlHttp = undefined;
/**
* Contact the server to fetch the set of autocomplete options for the
* projects the user is contributor/member/owner of.
* @param {multiValue} boolean If set to true, the projectStore is configured to
* have support for multi-values (useful for example for saved queries where
* a query can apply to multiple projects).
*/
function TKR_fetchUserProjects(multiValue) {
// Set a request token to prevent XSRF leaking of user project lists.
const userRefs = [{displayName: window.CS_env.loggedInUserEmail}];
const userProjectsPromise = window.prpcClient.call(
'monorail.Users', 'GetUsersProjects', {userRefs});
userProjectsPromise.then((response) => {
const userProjects = response.usersProjects[0];
const projects = (userProjects.ownerOf || [])
.concat(userProjects.memberOf || [])
.concat(userProjects.contributorTo || []);
projects.sort();
if (projects) {
TKR_setUpProjectStore(projects, multiValue);
}
});
}
/**
* Constuct a new autocomplete store with all the projects that the
* current user has visibility into. The store has some monorail-specific
* methods.
* @param {Array} projects An array of project names.
* @param {boolean} multiValue Determines whether the store should support
* multiple values.
*/
function TKR_setUpProjectStore(projects, multiValue) {
const projectsDefs = [];
const docdict = {};
for (let i = 0; i < projects.length; ++i) {
projectsDefs.push({'name': projects[i], 'doc': ''});
docdict[projects[i]] = '';
}
TKR_projectStore = new _AC_SimpleStore(projects, docdict);
TKR_projectStore.commaCompletes = !multiValue;
if (multiValue) {
TKR_projectStore.substitute = TKR_acSubstituteWithComma;
} else {
TKR_projectStore.substitute =
function(inputValue, cursor, completable, completion) {
return completion.value;
};
}
TKR_projectStore.completions = function(prefix, tofilter) {
const fullList = TKR_fullComplete(prefix, projectsDefs);
if (fullList) return fullList;
return _AC_SimpleStore.prototype.completions.call(this, prefix, tofilter);
};
TKR_projectStore.completable = function(inputValue, cursor) {
if (inputValue == '') return '*project';
if (multiValue) {
return _AC_SimpleStore.prototype.completable.call(
this, inputValue, cursor);
} else {
return inputValue;
}
};
}
/**
* Convert the object resulting of a monorail.Projects ListStatuses to
* the format expected by TKR_populateAutocomplete.
* @param {object} statusesResponse A pRPC ListStatusesResponse object.
*/
function TKR_convertStatuses(statusesResponse) {
const statusDefs = statusesResponse.statusDefs || [];
const jsonData = {};
// Split statusDefs into open and closed name-doc objects.
jsonData.open = [];
jsonData.closed = [];
for (const s of statusDefs) {
if (!s.deprecated) {
const item = {
name: s.status,
doc: s.docstring,
};
if (s.meansOpen) {
jsonData.open.push(item);
} else {
jsonData.closed.push(item);
}
}
}
jsonData.strict = statusesResponse.restrictToKnown;
return jsonData;
}
/**
* Convert the object resulting of a monorail.Projects ListComponents to
* the format expected by TKR_populateAutocomplete.
* @param {object} componentsResponse A pRPC ListComponentsResponse object.
*/
function TKR_convertComponents(componentsResponse) {
const componentDefs = (componentsResponse.componentDefs || []);
const jsonData = {};
// Filter out deprecated components and normalize to name-doc object.
jsonData.components = [];
for (const c of componentDefs) {
if (!c.deprecated) {
jsonData.components.push({
name: c.path,
doc: c.docstring,
});
}
}
return jsonData;
}
/**
* Convert the object resulting of a monorail.Projects GetLabelOptions
* call to the format expected by TKR_populateAutocomplete.
* @param {object} labelsResponse A pRPC GetLabelOptionsResponse.
* @param {Array<FieldDef>=} fieldDefs FieldDefs from a project config, used to
* mask labels that are used to implement custom enum fields.
*/
function TKR_convertLabels(labelsResponse, fieldDefs = []) {
const labelDefs = (labelsResponse.labelDefs || []);
const exclusiveLabelPrefixes = (labelsResponse.exclusiveLabelPrefixes || []);
const jsonData = {};
const maskedLabels = new Set();
fieldDefs.forEach((fd) => {
if (fd.enumChoices) {
fd.enumChoices.forEach(({label}) => {
maskedLabels.add(`${fd.fieldRef.fieldName}-${label}`);
});
}
});
jsonData.labels = labelDefs.filter(({label}) => !maskedLabels.has(label)).map(
(label) => ({name: label.label, doc: label.docstring}));
jsonData.excl_prefixes = exclusiveLabelPrefixes.map(
(prefix) => prefix.toLowerCase());
return jsonData;
}
/**
* Convert the object resulting of a monorail.Projects GetVisibleMembers
* call to the format expected by TKR_populateAutocomplete.
* @param {object?} visibleMembersResponse A pRPC GetVisibleMembersResponse.
* @return {{memberEmails: {name: string}, nonGroupEmails: {name: string}}}
*/
function TKR_convertVisibleMembers(visibleMembersResponse) {
if (!visibleMembersResponse) {
visibleMembersResponse = {};
}
const groupRefs = (visibleMembersResponse.groupRefs || []);
const userRefs = (visibleMembersResponse.userRefs || []);
const jsonData = {};
const groupEmails = new Set(groupRefs.map(
(groupRef) => groupRef.displayName));
jsonData.memberEmails = userRefs.map(
(userRef) => ({name: userRef.displayName}));
jsonData.nonGroupEmails = jsonData.memberEmails.filter(
(memberEmail) => !groupEmails.has(memberEmail));
return jsonData;
}
/**
* Convert the object resulting of a monorail.Projects ListFields to
* the format expected by TKR_populateAutocomplete.
* @param {object} fieldsResponse A pRPC ListFieldsResponse object.
*/
function TKR_convertFields(fieldsResponse) {
const fieldDefs = (fieldsResponse.fieldDefs || []);
const jsonData = {};
jsonData.fields = fieldDefs.map((field) =>
({
field_id: field.fieldRef.fieldId,
field_name: field.fieldRef.fieldName,
field_type: field.fieldRef.type,
docstring: field.docstring,
choices: (field.enumChoices || []).map(
(choice) => ({name: choice.label, doc: choice.docstring})),
qualifiedMembers: (field.userChoices || []).map(
(userRef) => ({name: userRef.displayName})),
}),
);
return jsonData;
}
/**
* Convert the object resulting of a monorail.Features ListHotlistsByUser
* call to the format expected by TKR_populateAutocomplete.
* @param {Array<HotlistV0>} hotlists A lists of hotlists
* @return {Array<{ref_str: string, summary: string}>}
*/
function TKR_convertHotlists(hotlists) {
if (hotlists === undefined) {
return [];
}
const seen = new Set();
const ambiguousNames = new Set();
hotlists.forEach((hotlist) => {
if (seen.has(hotlist.name)) {
ambiguousNames.add(hotlist.name);
}
seen.add(hotlist.name);
});
return hotlists.map((hotlist) => {
let ref_str = hotlist.name;
if (ambiguousNames.has(hotlist.name)) {
ref_str = hotlist.owner_ref.display_name + ':' + ref_str;
}
return {ref_str: ref_str, summary: hotlist.summary};
});
}
/**
* Initializes hotlists in autocomplete store.
* @param {Array<HotlistV0>} hotlists
*/
function TKR_populateHotlistAutocomplete(hotlists) {
TKR_setUpHotlistsStore(TKR_convertHotlists(hotlists));
}
/**
* Add project config data that's already been fetched to the legacy
* autocomplete.
* @param {Config} projectConfig Returned projectConfig data.
* @param {GetVisibleMembersResponse} visibleMembers
* @param {Array<string>} customPermissions
*/
function TKR_populateAutocomplete(projectConfig, visibleMembers,
customPermissions = []) {
const {statusDefs, componentDefs, labelDefs, fieldDefs,
exclusiveLabelPrefixes, projectName} = projectConfig;
const {memberEmails, nonGroupEmails} =
TKR_convertVisibleMembers(visibleMembers);
TKR_setUpMemberStore(memberEmails, nonGroupEmails);
TKR_prepOwnerField(memberEmails);
const {open, closed, strict} = TKR_convertStatuses({statusDefs});
TKR_setUpStatusStore(open, closed);
TKR_restrict_to_known = strict;
const {components} = TKR_convertComponents({componentDefs});
TKR_setUpComponentStore(components);
const {excl_prefixes, labels} = TKR_convertLabels(
{labelDefs, exclusiveLabelPrefixes}, fieldDefs);
TKR_exclPrefixes = excl_prefixes;
TKR_setUpLabelStore(labels);
const {fields} = TKR_convertFields({fieldDefs});
TKR_setUpUserAutocompleteStores(fields);
/* QuickEdit is not yet in Monorail. crbug.com/monorail/1926
TKR_setUpQuickEditStore(
jsonData.labels, jsonData.memberEmails, jsonData.open, jsonData.closed,
jsonData.nonGroupEmails);
*/
// We need to wait until both exclusive prefixes (in configPromise) and
// labels (in labelsPromise) have been read.
TKR_prepLabelAC(TKR_labelFieldIDPrefix);
TKR_setUpSearchStore(
labels, memberEmails, open, closed,
components, fields, nonGroupEmails);
TKR_setUpCustomPermissionsStore(customPermissions);
}