Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/shared/convertersV0.js b/static_src/shared/convertersV0.js
new file mode 100644
index 0000000..ffb8a36
--- /dev/null
+++ b/static_src/shared/convertersV0.js
@@ -0,0 +1,610 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview This file collects helpers for managing various canonical
+ * formats used within Monorail's frontend. When converting between common
+ * Objects, for example, it's recommended to use the helpers in this file
+ * to ensure consistency across conversions.
+ *
+ * Converters between v0 and v3 object types are included in this file
+ * as well.
+ */
+
+import qs from 'qs';
+
+import {equalsIgnoreCase, capitalizeFirst} from './helpers.js';
+import {fromShortlink} from 'shared/federated.js';
+import {UserInputError} from 'shared/errors.js';
+import './typedef.js';
+
+/**
+ * Common restriction labels to do things users frequently want to do
+ * with restrictions.
+ * This code is a frontend replication of old Python server code that
+ * hardcoded specific restriction labels.
+ * @type {Array<LabelDef>}
+ */
+const FREQUENT_ISSUE_RESTRICTIONS = Object.freeze([
+ {
+ label: 'Restrict-View-EditIssue',
+ docstring: 'Only users who can edit the issue may access it',
+ },
+ {
+ label: 'Restrict-AddIssueComment-EditIssue',
+ docstring: 'Only users who can edit the issue may add comments',
+ },
+]);
+
+/**
+ * The set of actions that permissions on an issue can be applied to.
+ * For example, in the Restrict-View-Google label, "View" is an action.
+ * @type {Array<string>}
+ */
+const STANDARD_ISSUE_ACTIONS = [
+ 'View', 'EditIssue', 'AddIssueComment', 'DeleteIssue', 'FlagSpam'];
+
+// A Regex defining the canonical String format used in Monorail for allowing
+// users to input structured localId and projectName values in free text inputs.
+// Match: projectName:localId format where projectName is optional.
+// ie: "monorail:1234" or "1234".
+const ISSUE_ID_REGEX = /(?:([a-z0-9-]+):)?(\d+)/i;
+
+// RFC 2821-compliant email address regex used by the server when validating
+// email addresses.
+// eslint-disable-next-line max-len
+const RFC_2821_EMAIL_REGEX = /^[-a-zA-Z0-9!#$%&'*+\/=?^_`{|}~]+(?:[.][-a-zA-Z0-9!#$%&'*+\/=?^_`{|}~]+)*@(?:(?:[0-9a-zA-Z](?:[-]*[0-9a-zA-Z]+)*)(?:\.[0-9a-zA-Z](?:[-]*[0-9a-zA-Z]+)*)*)\.(?:[a-zA-Z]{2,9})$/;
+
+/**
+ * Converts a displayName into a canonical UserRef Object format.
+ *
+ * @param {string} displayName The user's email address, used as a display name.
+ * @return {UserRef} UserRef formatted object that contains a
+ * user's displayName.
+ */
+export function displayNameToUserRef(displayName) {
+ if (displayName && !RFC_2821_EMAIL_REGEX.test(displayName)) {
+ throw new UserInputError(`Invalid email address: ${displayName}`);
+ }
+ return {displayName};
+}
+
+/**
+ * Converts a displayName into a canonical UserRef Object format.
+ *
+ * @param {string} user The user's email address, used as a display name,
+ * or their numeric user ID.
+ * @return {UserRef} UserRef formatted object that contains a
+ * user's displayName or userId.
+ */
+export function userIdOrDisplayNameToUserRef(user) {
+ if (RFC_2821_EMAIL_REGEX.test(user)) {
+ return {displayName: user};
+ }
+ const userId = Number.parseInt(user);
+ if (Number.isNaN(userId)) {
+ throw new UserInputError(`Invalid email address or user ID: ${user}`);
+ }
+ return {userId};
+}
+
+/**
+ * Converts an Object into a standard UserRef Object with only a displayName
+ * and userId. Used for cases when we need to use only the data required to
+ * identify a unique user, such as when requesting information related to a user
+ * through the API.
+ *
+ * @param {UserV0} user An Object representing a user, in the JSON format
+ * returned by the pRPC API.
+ * @return {UserRef} UserRef style Object.
+ */
+export function userToUserRef(user) {
+ if (!user) return {};
+ const {userId, displayName} = user;
+ return {userId, displayName};
+}
+
+/**
+ * Converts a User resource name to a numeric user ID.
+ * @param {string} name
+ * @return {number}
+ */
+export function userNameToId(name) {
+ return Number.parseInt(name.split('/')[1]);
+}
+
+/**
+ * Converts a v3 API User object to a v0 API UserRef.
+ * @param {User} user
+ * @return {UserRef}
+ */
+export function userV3ToRef(user) {
+ return {userId: userNameToId(user.name), displayName: user.displayName};
+}
+
+/**
+ * Convert a UserRef style Object to a userId string.
+ *
+ * @param {UserRef} userRef Object expected to contain a userId key.
+ * @return {number} the unique ID of the user.
+ */
+export function userRefToId(userRef) {
+ return userRef && userRef.userId;
+}
+
+/**
+ * Extracts the displayName property from a UserRef Object.
+ *
+ * @param {UserRef} userRef UserRef Object uniquely identifying a user.
+ * @return {string} The user's display name (email address).
+ */
+export function userRefToDisplayName(userRef) {
+ return userRef && userRef.displayName;
+}
+
+/**
+ * Converts an Array of UserRefs to an Array of display name Strings.
+ *
+ * @param {Array<UserRef>} userRefs Array of UserRefs.
+ * @return {Array<string>} Array of display names.
+ */
+export function userRefsToDisplayNames(userRefs) {
+ if (!userRefs) return [];
+ return userRefs.map(userRefToDisplayName);
+}
+
+/**
+ * Takes an Array of UserRefs and keeps only UserRefs where ID
+ * is known.
+ *
+ * @param {Array<UserRef>} userRefs Array of UserRefs.
+ * @return {Array<UserRef>} Filtered Array IDs guaranteed.
+ */
+export function userRefsWithIds(userRefs) {
+ if (!userRefs) return [];
+ return userRefs.filter((u) => u.userId);
+}
+
+/**
+ * Takes an Array of UserRefs and returns displayNames for
+ * only those refs with IDs specified.
+ *
+ * @param {Array<UserRef>} userRefs Array of UserRefs.
+ * @return {Array<string>} Array of user displayNames.
+ */
+export function filteredUserDisplayNames(userRefs) {
+ if (!userRefs) return [];
+ return userRefsToDisplayNames(userRefsWithIds(userRefs));
+}
+
+/**
+ * Takes in the name of a label and turns it into a LabelRef Object.
+ *
+ * @param {string} label The name of a label.
+ * @return {LabelRef}
+ */
+export function labelStringToRef(label) {
+ return {label};
+}
+
+/**
+ * Takes in the name of a label and turns it into a LabelRef Object.
+ *
+ * @param {LabelRef} labelRef
+ * @return {string} The name of the label.
+ */
+export function labelRefToString(labelRef) {
+ if (!labelRef) return;
+ return labelRef.label;
+}
+
+/**
+ * Converts an Array of LabelRef Objects to label name Strings.
+ *
+ * @param {Array<LabelRef>} labelRefs Array of LabelRef Objects.
+ * @return {Array<string>} Array of label names.
+ */
+export function labelRefsToStrings(labelRefs) {
+ if (!labelRefs) return [];
+ return labelRefs.map(labelRefToString);
+}
+
+/**
+ * Filters a list of labels into a list of only labels with one word.
+ *
+ * @param {Array<LabelRef>} labelRefs
+ * @return {Array<LabelRef>} only the LabelRefs that do not have multiple words.
+ */
+export function labelRefsToOneWordLabels(labelRefs) {
+ if (!labelRefs) return [];
+ return labelRefs.filter(({label}) => {
+ return isOneWordLabel(label);
+ });
+}
+
+/**
+ * Checks whether a particular label is one word.
+ *
+ * @param {string} label the name of the label being checked.
+ * @return {boolean} Whether the label is one word or not.
+ */
+export function isOneWordLabel(label = '') {
+ const words = label.split('-');
+ return words.length === 1;
+}
+
+/**
+ * Creates a LabelDef Object for a restriction label given an action
+ * and a permission.
+ * @param {string} action What action a restriction is applied to.
+ * eg. "View", "EditIssue", "AddIssueComment".
+ * @param {string} permission The permission group that has access to
+ * the restricted behavior. eg. "Google".
+ * @return {LabelDef}
+ */
+export function _makeRestrictionLabel(action, permission) {
+ const perm = capitalizeFirst(permission);
+ return {
+ label: `Restrict-${action}-${perm}`,
+ docstring: `Permission ${perm} needed to use ${action}`,
+ };
+}
+
+/**
+ * Given a list of custom permissions defined for a project, this function
+ * generates simulated LabelDef objects for those permissions + default
+ * restriction labels that all projects should have.
+ * @param {Array<string>=} customPermissions
+ * @param {Array<string>=} actions
+ * @param {Array<LabelDef>=} defaultRestrictionLabels Configurable default
+ * restriction labels to include regardless of custom permissions.
+ * @return {Array<LabelDef>}
+ */
+export function restrictionLabelsForPermissions(customPermissions = [],
+ actions = STANDARD_ISSUE_ACTIONS,
+ defaultRestrictionLabels = FREQUENT_ISSUE_RESTRICTIONS) {
+ const labels = [];
+ actions.forEach((action) => {
+ customPermissions.forEach((permission) => {
+ labels.push(_makeRestrictionLabel(action, permission));
+ });
+ });
+ return [...labels, ...defaultRestrictionLabels];
+}
+
+/**
+ * Converts a custom field name in to the prefix format used in
+ * enum type field values. Monorail defines the enum options for
+ * a custom field as labels.
+ *
+ * @param {string} fieldName Name of a custom field.
+ * @return {string} The label prefixes for enum choices
+ * associated with the field.
+ */
+export function fieldNameToLabelPrefix(fieldName) {
+ return `${fieldName.toLowerCase()}-`;
+}
+
+/**
+ * Finds all prefixes in a label's name, delimited by '-'. A given label
+ * can have multiple possible prefixes, one for each instance of '-'.
+ * Labels that share the same prefix are implicitly treated like
+ * enum fields in certain parts of Monorail's UI.
+ *
+ * @param {string} label The name of the label.
+ * @return {Array<string>} All prefixes in the label.
+ */
+export function labelNameToLabelPrefixes(label) {
+ if (!label) return;
+ const prefixes = [];
+ for (let i = 0; i < label.length; i++) {
+ if (label[i] === '-') {
+ prefixes.push(label.substring(0, i));
+ }
+ }
+ return prefixes;
+}
+
+/**
+ * Truncates a label to include only the label's value, delimited
+ * by '-'.
+ *
+ * @param {string} label The name of the label.
+ * @param {string} fieldName The field name that the label is having a
+ * value extracted for.
+ * @return {string} The label's value.
+ */
+export function labelNameToLabelValue(label, fieldName) {
+ if (!label || !fieldName || isOneWordLabel(label)) return null;
+ const prefix = fieldName.toLowerCase() + '-';
+ if (!label.toLowerCase().startsWith(prefix)) return null;
+
+ return label.substring(prefix.length);
+}
+
+/**
+ * Converts a FieldDef to a v3 FieldDef resource name.
+ * @param {string} projectName The name of the project.
+ * @param {FieldDef} fieldDef A FieldDef Object from the pRPC API proto objects.
+ * @return {string} The v3 FieldDef name, e.g. 'projects/proj/fieldDefs/fieldId'
+ */
+export function fieldDefToName(projectName, fieldDef) {
+ return `projects/${projectName}/fieldDefs/${fieldDef.fieldRef.fieldId}`;
+}
+
+/**
+ * Extracts just the name of the status from a StatusRef Object.
+ *
+ * @param {StatusRef} statusRef
+ * @return {string} The name of the status.
+ */
+export function statusRefToString(statusRef) {
+ return statusRef.status;
+}
+
+/**
+ * Extracts the name of multiple statuses from multiple StatusRef Objects.
+ *
+ * @param {Array<StatusRef>} statusRefs
+ * @return {Array<string>} The names of the statuses inputted.
+ */
+export function statusRefsToStrings(statusRefs) {
+ return statusRefs.map(statusRefToString);
+}
+
+/**
+ * Takes the name of a component and converts it into a ComponentRef
+ * Object.
+ *
+ * @param {string} path Name of the component.
+ * @return {ComponentRef}
+ */
+export function componentStringToRef(path) {
+ return {path};
+}
+
+/**
+ * Extracts just the name of a component from a ComponentRef.
+ *
+ * @param {ComponentRef} componentRef
+ * @return {string} The name of the component.
+ */
+export function componentRefToString(componentRef) {
+ return componentRef && componentRef.path;
+}
+
+/**
+ * Extracts the names of multiple components from multiple refs.
+ *
+ * @param {Array<ComponentRef>} componentRefs
+ * @return {Array<string>} Array of component names.
+ */
+export function componentRefsToStrings(componentRefs) {
+ if (!componentRefs) return [];
+ return componentRefs.map(componentRefToString);
+}
+
+/**
+ * Takes a String with a project name and issue ID in Monorail's canonical
+ * IssueRef format and converts it into an IssueRef Object.
+ *
+ * @param {IssueRefString} idStr A String of the format projectName:1234, a
+ * standard issue ID input format used across Monorail.
+ * @param {string=} defaultProjectName The implied projectName if none is
+ * specified.
+ * @return {IssueRef}
+ * @throws {UserInputError} If the IssueRef string is invalidly formatted.
+ */
+export function issueStringToRef(idStr, defaultProjectName) {
+ if (!idStr) return {};
+
+ // If the string includes a slash, it's an external tracker ref.
+ if (idStr.includes('/')) {
+ return {extIdentifier: idStr};
+ }
+
+ const matches = idStr.match(ISSUE_ID_REGEX);
+ if (!matches) {
+ throw new UserInputError(
+ `Invalid issue ref: ${idStr}. Expected [projectName:]issueId.`);
+ }
+ const projectName = matches[1] ? matches[1] : defaultProjectName;
+
+ if (!projectName) {
+ throw new UserInputError(
+ `Issue ref must include a project name or specify a default project.`);
+ }
+
+ const localId = Number.parseInt(matches[2]);
+ return {localId, projectName};
+}
+
+/**
+ * Takes an IssueRefString and converts it into an IssueRef Object, checking
+ * that it's not the same as another specified issueRef. ie: validates that an
+ * inputted blocking issue is not the same as the issue being blocked.
+ *
+ * @param {IssueRef} issueRef The issue that the IssueRefString is being
+ * compared to.
+ * @param {IssueRefString} idStr A String of the format projectName:1234, a
+ * standard issue ID input format used across Monorail.
+ * @return {IssueRef}
+ * @throws {UserInputError} If the IssueRef string is invalidly formatted
+ * or if the issue is equivalent to the linked issue.
+ */
+export function issueStringToBlockingRef(issueRef, idStr) {
+ // TODO(zhangtiff): Consider simplifying this helper function to only validate
+ // that an issue does not block itself rather than also doing string parsing.
+ const result = issueStringToRef(idStr, issueRef.projectName);
+ if (result.projectName === issueRef.projectName &&
+ result.localId === issueRef.localId) {
+ throw new UserInputError(
+ `Invalid issue ref: ${idStr
+ }. Cannot merge or block an issue on itself.`);
+ }
+ return result;
+}
+
+/**
+ * Converts an IssueRef into a canonical String format. ie: "project:1234"
+ *
+ * @param {IssueRef} ref
+ * @param {string=} projectName The current project context. The
+ * generated String excludes the projectName if it matches the
+ * project the user is currently viewing, to create simpler
+ * issue ID links.
+ * @return {IssueRefString} A String representing the pieces of an IssueRef.
+ */
+export function issueRefToString(ref, projectName = undefined) {
+ if (!ref) return '';
+
+ if (ref.hasOwnProperty('extIdentifier')) {
+ return ref.extIdentifier;
+ }
+
+ if (projectName && projectName.length &&
+ equalsIgnoreCase(ref.projectName, projectName)) {
+ return `${ref.localId}`;
+ }
+ return `${ref.projectName}:${ref.localId}`;
+}
+
+/**
+ * Converts a full Issue Object into only the pieces of its data needed
+ * to define an IssueRef. Useful for cases when we don't want to send excess
+ * information to ifentify an Issue.
+ *
+ * @param {Issue} issue A full Issue Object.
+ * @return {IssueRef} Just the ID part of the Issue Object.
+ */
+export function issueToIssueRef(issue) {
+ if (!issue) return {};
+
+ return {localId: issue.localId,
+ projectName: issue.projectName};
+}
+
+/**
+ * Converts a full Issue Object into an IssueRefString
+ *
+ * @param {Issue} issue A full Issue Object.
+ * @param {string=} defaultProjectName The default project the String should
+ * assume.
+ * @return {IssueRefString} A String with all the data needed to
+ * construct an IssueRef.
+ */
+export function issueToIssueRefString(issue, defaultProjectName = undefined) {
+ if (!issue) return '';
+
+ const ref = issueToIssueRef(issue);
+ return issueRefToString(ref, defaultProjectName);
+}
+
+/**
+ * Creates a link to a particular issue specified in an IssueRef.
+ *
+ * @param {IssueRef} ref The issue that the generated URL will point to.
+ * @param {Object} queryParams The URL params for the URL.
+ * @return {string} The URL for the issue's page as a relative path.
+ */
+export function issueRefToUrl(ref, queryParams = {}) {
+ const queryParamsCopy = {...queryParams};
+
+ if (!ref) return '';
+
+ if (ref.extIdentifier) {
+ const extRef = fromShortlink(ref.extIdentifier);
+ if (!extRef) {
+ console.error(`No tracker found for reference: ${ref.extIdentifier}`);
+ return '';
+ }
+ return extRef.toURL();
+ }
+
+ let paramString = '';
+ if (Object.keys(queryParamsCopy).length) {
+ delete queryParamsCopy.id;
+
+ paramString = `&${qs.stringify(queryParamsCopy)}`;
+ }
+
+ return `/p/${ref.projectName}/issues/detail?id=${ref.localId}${paramString}`;
+}
+
+/**
+ * Converts multiple IssueRef Objects into Strings in the canonical IssueRef
+ * String form expeced by Monorail.
+ *
+ * @param {Array<IssueRef>} arr Array of IssueRefs to convert to Strings.
+ * @param {string} projectName The default project name.
+ * @return {Array<IssueRefString>} Array of Strings where each entry is
+ * represents one IssueRef.
+ */
+export function issueRefsToStrings(arr, projectName) {
+ if (!arr || !arr.length) return [];
+ return arr.map((ref) => issueRefToString(ref, projectName));
+}
+
+/**
+ * Converts an issue name in the v3 API to an IssueRef in the v0 API.
+ * @param {string} name The v3 Issue name, e.g. 'projects/proj-name/issues/123'
+ * @return {IssueRef} An IssueRef.
+ */
+export function issueNameToRef(name) {
+ const nameParts = name.split('/');
+ return {
+ projectName: nameParts[1],
+ localId: parseInt(nameParts[3]),
+ };
+}
+
+/**
+ * Converts an issue name in the v3 API to an IssueRefString in the v0 API.
+ * @param {string} name The v3 Issue name, e.g. 'projects/proj-name/issues/123'
+ * @return {IssueRefString} A String with all the data needed to
+ * construct an IssueRef.
+ */
+export function issueNameToRefString(name) {
+ const nameParts = name.split('/');
+ return `${nameParts[1]}:${nameParts[3]}`;
+}
+
+/**
+ * Converts an v0 Issue to a v3 Issue name.
+ * @param {Issue} issue An Issue Object from the pRPC API issue_objects.proto.
+ * @return {string} The v3 Issue name, e.g. 'projects/proj-name/issues/123'
+ */
+export function issueToName(issue) {
+ return `projects/${issue.projectName}/issues/${issue.localId}`;
+}
+
+/**
+ * Since Monorail stores issue descriptions and description updates as comments,
+ * this function exists to filter a list of comments to get only those comments
+ * that are marked as descriptions.
+ *
+ * @param {Array<IssueComment>} comments List of many comments, usually all
+ * comments associated with an issue.
+ * @return {Array<IssueComment>} List of only the comments that are
+ * descriptions.
+ */
+export function commentListToDescriptionList(comments) {
+ if (!comments) return [];
+ // First comment is always a description, even if it doesn't have a
+ // descriptionNum.
+ return comments.filter((c, i) => !i || c.descriptionNum);
+}
+
+/**
+ * Wraps a String value for a field and a FieldRef into a FieldValue
+ * Object.
+ *
+ * @param {FieldRef} fieldRef A reference to the custom field that this
+ * value is tied to.
+ * @param {string} value The value associated with the FieldRef.
+ * @return {FieldValue}
+ */
+export function valueToFieldValue(fieldRef, value) {
+ return {fieldRef, value};
+}