Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/static_src/shared/helpers.js b/static_src/shared/helpers.js
new file mode 100644
index 0000000..362b4ec
--- /dev/null
+++ b/static_src/shared/helpers.js
@@ -0,0 +1,213 @@
+// 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.
+
+import qs from 'qs';
+
+
+/**
+ * With lists a and b, get the elements that are in a but not in b.
+ * result = a - b
+ * @param {Array} listA
+ * @param {Array} listB
+ * @param {function?} equals
+ * @return {Array}
+ */
+export function arrayDifference(listA, listB, equals = undefined) {
+ if (!equals) {
+ equals = (a, b) => (a === b);
+ }
+ listA = listA || [];
+ listB = listB || [];
+ return listA.filter((a) => {
+ return !listB.find((b) => (equals(a, b)));
+ });
+}
+
+/**
+ * Check to see if a Set contains any of a list of values.
+ *
+ * @param {Set} set the Set to check for values in.
+ * @param {Iterable} values checks if any of these values are included.
+ * @return {boolean} whether the Set has any of the values or not.
+ */
+export function setHasAny(set, values) {
+ for (const value of values) {
+ if (set.has(value)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Capitalize the first letter of a given string.
+ * @param {string} str
+ * @return {string}
+ */
+export function capitalizeFirst(str) {
+ return `${str.charAt(0).toUpperCase()}${str.substring(1)}`;
+}
+
+/**
+ * Check if a string has a prefix, ignoring case.
+ * @param {string} str
+ * @param {string} prefix
+ * @return {boolean}
+ */
+export function hasPrefix(str, prefix) {
+ return str.toLowerCase().startsWith(prefix.toLowerCase());
+}
+
+/**
+ * Returns a string without specified prefix
+ * @param {string} str
+ * @param {string} prefix
+ * @return {string}
+ */
+export function removePrefix(str, prefix) {
+ return str.substr(prefix.length);
+}
+
+// TODO(zhangtiff): Make this more grammatically correct for
+// more than two items.
+export function arrayToEnglish(arr) {
+ if (!arr) return '';
+ return arr.join(' and ');
+}
+
+export function pluralize(count, singular, pluralArg) {
+ const plural = pluralArg || singular + 's';
+ return count === 1 ? singular : plural;
+}
+
+export function objectToMap(obj = {}) {
+ const map = new Map();
+ Object.keys(obj).forEach((key) => {
+ map.set(key, obj[key]);
+ });
+ return map;
+}
+
+/**
+ * Given an Object, extract a list of values from it, based on some
+ * specified keys.
+ *
+ * @param {Object} obj the Object to read values from.
+ * @param {Array} keys the Object keys to fetch values for.
+ * @return {Array} Object values matching the given keys.
+ */
+export function objectValuesForKeys(obj, keys = []) {
+ return keys.map((key) => ((key in obj) ? obj[key] : undefined));
+}
+
+/**
+ * Checks to see if object has no keys
+ * @param {Object} obj
+ * @return {boolean}
+ */
+export function isEmptyObject(obj) {
+ return Object.keys(obj).length === 0;
+}
+
+/**
+ * Checks if two strings are equal, case-insensitive
+ * @param {string} a
+ * @param {string} b
+ * @return {boolean}
+ */
+export function equalsIgnoreCase(a, b) {
+ if (a == b) return true;
+ if (!a || !b) return false;
+ return a.toLowerCase() === b.toLowerCase();
+}
+
+export function immutableSplice(arr, index, count, ...addedItems) {
+ if (!arr) return '';
+
+ return [...arr.slice(0, index), ...addedItems, ...arr.slice(index + count)];
+}
+
+/**
+ * Computes a new URL for a page based on an exiting path and set of query
+ * params.
+ *
+ * @param {string} baseUrl the base URL without query params.
+ * @param {Object} oldParams original query params before changes.
+ * @param {Object} newParams query parameters to override existing ones.
+ * @param {Array} deletedParams list of keys to be cleared.
+ * @return {string} the new URL with the updated params.
+ */
+export function urlWithNewParams(baseUrl = '',
+ oldParams = {}, newParams = {}, deletedParams = []) {
+ const params = {...oldParams, ...newParams};
+ deletedParams.forEach((name) => {
+ delete params[name];
+ });
+
+ const queryString = qs.stringify(params);
+
+ return `${baseUrl}${queryString ? '?' : ''}${queryString}`;
+}
+
+/**
+ * Finds out whether a user is a member of a given project based on
+ * project membership info.
+ *
+ * @param {Object} userRef reference to a given user. Expects an id.
+ * @param {string} projectName name of the project being searched for.
+ * @param {Map} usersProjects all known user project memberships where
+ * keys are userId and values are Objects with expected values
+ * for {ownerOf, memberOf, contributorTo}.
+ * @return {boolean} whether the user is a member of the project or not.
+ */
+export function userIsMember(userRef, projectName, usersProjects = new Map()) {
+ // TODO(crbug.com/monorail/5968): Find a better place to place this function
+ if (!userRef || !userRef.userId || !projectName) return false;
+ const userProjects = usersProjects.get(userRef.userId);
+ if (!userProjects) return false;
+ const {ownerOf = [], memberOf = [], contributorTo = []} = userProjects;
+ return ownerOf.includes(projectName) ||
+ memberOf.includes(projectName) ||
+ contributorTo.includes(projectName);
+}
+
+/**
+ * Creates a function that checks two objects are not equal
+ * based on a set of property keys
+ *
+ * @param {Set<string>} props
+ * @return {function(): boolean}
+ */
+export function createObjectComparisonFunc(props) {
+ /**
+ * Computes whether set of properties have changed
+ * @param {Object<string, string>} newVal
+ * @param {Object<string, string>} oldVal
+ * @return {boolean}
+ */
+ return function(newVal, oldVal) {
+ if (oldVal === undefined && newVal === undefined) {
+ return false;
+ } else if (oldVal === undefined || newVal === undefined) {
+ return true;
+ } else if (oldVal === null && newVal === null) {
+ return false;
+ } else if (oldVal === null || newVal === null) {
+ return true;
+ }
+
+ return Array.from(props)
+ .some((propName) => newVal[propName] !== oldVal[propName]);
+ };
+}
+
+/**
+ * Calculates whether to wait for memberDefaultQuery to exist prior
+ * to fetching IssueList. Logged in users may use a default query.
+ * @param {Object} queryParams
+ * @return {boolean}
+ */
+export const shouldWaitForDefaultQuery = (queryParams) => {
+ return !queryParams.hasOwnProperty('q');
+};