blob: 968d05e372d823900648bd921aa1241d443290ad [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001// Copyright 2019 The Chromium Authors
Copybara854996b2021-09-07 19:36:02 +00002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import qs from 'qs';
6
7
8/**
9 * With lists a and b, get the elements that are in a but not in b.
10 * result = a - b
11 * @param {Array} listA
12 * @param {Array} listB
13 * @param {function?} equals
14 * @return {Array}
15 */
16export function arrayDifference(listA, listB, equals = undefined) {
17 if (!equals) {
18 equals = (a, b) => (a === b);
19 }
20 listA = listA || [];
21 listB = listB || [];
22 return listA.filter((a) => {
23 return !listB.find((b) => (equals(a, b)));
24 });
25}
26
27/**
28 * Check to see if a Set contains any of a list of values.
29 *
30 * @param {Set} set the Set to check for values in.
31 * @param {Iterable} values checks if any of these values are included.
32 * @return {boolean} whether the Set has any of the values or not.
33 */
34export function setHasAny(set, values) {
35 for (const value of values) {
36 if (set.has(value)) {
37 return true;
38 }
39 }
40 return false;
41}
42
43/**
44 * Capitalize the first letter of a given string.
45 * @param {string} str
46 * @return {string}
47 */
48export function capitalizeFirst(str) {
49 return `${str.charAt(0).toUpperCase()}${str.substring(1)}`;
50}
51
52/**
53 * Check if a string has a prefix, ignoring case.
54 * @param {string} str
55 * @param {string} prefix
56 * @return {boolean}
57 */
58export function hasPrefix(str, prefix) {
59 return str.toLowerCase().startsWith(prefix.toLowerCase());
60}
61
62/**
63 * Returns a string without specified prefix
64 * @param {string} str
65 * @param {string} prefix
66 * @return {string}
67 */
68export function removePrefix(str, prefix) {
69 return str.substr(prefix.length);
70}
71
72// TODO(zhangtiff): Make this more grammatically correct for
73// more than two items.
74export function arrayToEnglish(arr) {
75 if (!arr) return '';
76 return arr.join(' and ');
77}
78
79export function pluralize(count, singular, pluralArg) {
80 const plural = pluralArg || singular + 's';
81 return count === 1 ? singular : plural;
82}
83
84export function objectToMap(obj = {}) {
85 const map = new Map();
86 Object.keys(obj).forEach((key) => {
87 map.set(key, obj[key]);
88 });
89 return map;
90}
91
92/**
93 * Given an Object, extract a list of values from it, based on some
94 * specified keys.
95 *
96 * @param {Object} obj the Object to read values from.
97 * @param {Array} keys the Object keys to fetch values for.
98 * @return {Array} Object values matching the given keys.
99 */
100export function objectValuesForKeys(obj, keys = []) {
101 return keys.map((key) => ((key in obj) ? obj[key] : undefined));
102}
103
104/**
105 * Checks to see if object has no keys
106 * @param {Object} obj
107 * @return {boolean}
108 */
109export function isEmptyObject(obj) {
110 return Object.keys(obj).length === 0;
111}
112
113/**
114 * Checks if two strings are equal, case-insensitive
115 * @param {string} a
116 * @param {string} b
117 * @return {boolean}
118 */
119export function equalsIgnoreCase(a, b) {
120 if (a == b) return true;
121 if (!a || !b) return false;
122 return a.toLowerCase() === b.toLowerCase();
123}
124
125export function immutableSplice(arr, index, count, ...addedItems) {
126 if (!arr) return '';
127
128 return [...arr.slice(0, index), ...addedItems, ...arr.slice(index + count)];
129}
130
131/**
132 * Computes a new URL for a page based on an exiting path and set of query
133 * params.
134 *
135 * @param {string} baseUrl the base URL without query params.
136 * @param {Object} oldParams original query params before changes.
137 * @param {Object} newParams query parameters to override existing ones.
138 * @param {Array} deletedParams list of keys to be cleared.
139 * @return {string} the new URL with the updated params.
140 */
141export function urlWithNewParams(baseUrl = '',
142 oldParams = {}, newParams = {}, deletedParams = []) {
143 const params = {...oldParams, ...newParams};
144 deletedParams.forEach((name) => {
145 delete params[name];
146 });
147
148 const queryString = qs.stringify(params);
149
150 return `${baseUrl}${queryString ? '?' : ''}${queryString}`;
151}
152
153/**
154 * Finds out whether a user is a member of a given project based on
155 * project membership info.
156 *
157 * @param {Object} userRef reference to a given user. Expects an id.
158 * @param {string} projectName name of the project being searched for.
159 * @param {Map} usersProjects all known user project memberships where
160 * keys are userId and values are Objects with expected values
161 * for {ownerOf, memberOf, contributorTo}.
162 * @return {boolean} whether the user is a member of the project or not.
163 */
164export function userIsMember(userRef, projectName, usersProjects = new Map()) {
165 // TODO(crbug.com/monorail/5968): Find a better place to place this function
166 if (!userRef || !userRef.userId || !projectName) return false;
167 const userProjects = usersProjects.get(userRef.userId);
168 if (!userProjects) return false;
169 const {ownerOf = [], memberOf = [], contributorTo = []} = userProjects;
170 return ownerOf.includes(projectName) ||
171 memberOf.includes(projectName) ||
172 contributorTo.includes(projectName);
173}
174
175/**
176 * Creates a function that checks two objects are not equal
177 * based on a set of property keys
178 *
179 * @param {Set<string>} props
180 * @return {function(): boolean}
181 */
182export function createObjectComparisonFunc(props) {
183 /**
184 * Computes whether set of properties have changed
185 * @param {Object<string, string>} newVal
186 * @param {Object<string, string>} oldVal
187 * @return {boolean}
188 */
189 return function(newVal, oldVal) {
190 if (oldVal === undefined && newVal === undefined) {
191 return false;
192 } else if (oldVal === undefined || newVal === undefined) {
193 return true;
194 } else if (oldVal === null && newVal === null) {
195 return false;
196 } else if (oldVal === null || newVal === null) {
197 return true;
198 }
199
200 return Array.from(props)
201 .some((propName) => newVal[propName] !== oldVal[propName]);
202 };
203}
204
205/**
206 * Calculates whether to wait for memberDefaultQuery to exist prior
207 * to fetching IssueList. Logged in users may use a default query.
208 * @param {Object} queryParams
209 * @return {boolean}
210 */
211export const shouldWaitForDefaultQuery = (queryParams) => {
212 return !queryParams.hasOwnProperty('q');
213};
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100214
215// constant value for required redirect project
216const redirectProjects = Object.freeze(['pigweed', 'git', 'gerrit', 'skia', 'fuchsia']);
217
218/**
219 * Generate the url link for issue in project.
220 * @param {string} projectName Name of the project.
221 * @param {string} subUrl the sub URL without query params.
222 * @param {Object} params the query params.
223 * @return {string} the new URL
224 */
225export function generateProjectIssueURL(projectName, subPath, params = {}) {
226 const queryString = window.location.search;
227 const urlParams = new URLSearchParams(queryString);
228 const noRedirect = urlParams.has('no_tracker_redirect');
229 let baseUrl = '';
230 if (!noRedirect && redirectProjects.includes(projectName)) {
231 // Full url path will trigger backend service call to handle redirect.
232 baseUrl = 'https://bugs.chromium.org/p/' + projectName + '/issues' + subPath;
233 return urlWithNewParams(baseUrl, params, {}, undefined)
234 } else {
235 baseUrl = '/p/' + projectName + '/issues' + subPath;
236 const noRedirectParam = noRedirect ? {'no_tracker_redirect' : 1} : undefined
237 return urlWithNewParams(baseUrl, params, noRedirectParam, undefined)
238 }
239}