summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-20 09:07:57 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-20 09:07:57 +0000
commit7881eb30eaa8b01dbcfe87faa09927c75c7d6e45 (patch)
tree298bc8d2c62b2f2c29cb8ecbcf3de3eaaa6466d9 /app/assets/javascripts/lib
parent64b66e0cb6d1bfd27abf24e06653f00bddb60597 (diff)
downloadgitlab-ce-7881eb30eaa8b01dbcfe87faa09927c75c7d6e45.tar.gz
Add latest changes from gitlab-org/gitlab@12-6-stable-ee
Diffstat (limited to 'app/assets/javascripts/lib')
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js4
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js22
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js162
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js1
-rw-r--r--app/assets/javascripts/lib/utils/logoutput_behaviours.js47
-rw-r--r--app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js38
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js104
9 files changed, 225 insertions, 159 deletions
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index a04fe609015..4eec5bffc66 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -33,11 +33,9 @@ window.addEventListener('beforeunload', () => {
// Ignore AJAX errors caused by requests
// being cancelled due to browser navigation
-const { gon } = window;
-const featureFlagEnabled = gon && gon.features && gon.features.suppressAjaxNavigationErrors;
axios.interceptors.response.use(
response => response,
- err => suppressAjaxErrorsDuringNavigation(err, isUserNavigating, featureFlagEnabled),
+ err => suppressAjaxErrorsDuringNavigation(err, isUserNavigating),
);
export default axios;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 177ae4f9838..e4001e94478 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -5,7 +5,7 @@
import $ from 'jquery';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
-import { convertToCamelCase } from './text_utility';
+import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import breakpointInstance from '../../breakpoints';
@@ -490,6 +490,8 @@ export const historyPushState = newUrl => {
*/
export const parseBoolean = value => (value && value.toString()) === 'true';
+export const BACKOFF_TIMEOUT = 'BACKOFF_TIMEOUT';
+
/**
* @callback backOffCallback
* @param {Function} next
@@ -541,7 +543,7 @@ export const backOff = (fn, timeout = 60000) => {
timeElapsed += nextInterval;
nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
} else {
- reject(new Error('BACKOFF_TIMEOUT'));
+ reject(new Error(BACKOFF_TIMEOUT));
}
};
@@ -697,6 +699,22 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => {
}, initial);
};
+/**
+ * Converts all the object keys to snake case
+ *
+ * @param {Object} obj Object to transform
+ * @returns {Object}
+ */
+// Follow up to add additional options param:
+// https://gitlab.com/gitlab-org/gitlab/issues/39173
+export const convertObjectPropsToSnakeCase = (obj = {}) =>
+ obj
+ ? Object.entries(obj).reduce(
+ (acc, [key, value]) => ({ ...acc, [convertToSnakeCase(key)]: value }),
+ {},
+ )
+ : {};
+
export const imagePath = imgUrl =>
`${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`;
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 28143859e4c..996692bacb3 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import _ from 'underscore';
-import timeago from 'timeago.js';
+import * as timeago from 'timeago.js';
import dateFormat from 'dateformat';
import { languageCode, s__, __, n__ } from '../../locale';
@@ -92,90 +92,80 @@ export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => {
*/
const timeagoLanguageCode = languageCode().replace(/-/g, '_');
-let timeagoInstance;
-
/**
- * Sets a timeago Instance
+ * Registers timeago locales
*/
-export const getTimeago = () => {
- if (!timeagoInstance) {
- const memoizedLocaleRemaining = () => {
- const cache = [];
-
- const timeAgoLocaleRemaining = [
- () => [s__('Timeago|just now'), s__('Timeago|right now')],
- () => [s__('Timeago|just now'), s__('Timeago|%s seconds remaining')],
- () => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
- () => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
- () => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
- () => [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
- () => [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
- () => [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')],
- () => [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')],
- () => [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')],
- () => [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')],
- () => [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')],
- () => [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
- () => [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
- ];
-
- return (number, index) => {
- if (cache[index]) {
- return cache[index];
- }
- cache[index] = timeAgoLocaleRemaining[index] && timeAgoLocaleRemaining[index]();
- return cache[index];
- };
- };
-
- const memoizedLocale = () => {
- const cache = [];
-
- const timeAgoLocale = [
- () => [s__('Timeago|just now'), s__('Timeago|right now')],
- () => [s__('Timeago|just now'), s__('Timeago|in %s seconds')],
- () => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
- () => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
- () => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
- () => [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
- () => [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
- () => [s__('Timeago|%s days ago'), s__('Timeago|in %s days')],
- () => [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')],
- () => [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')],
- () => [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')],
- () => [s__('Timeago|%s months ago'), s__('Timeago|in %s months')],
- () => [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
- () => [s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
- ];
-
- return (number, index) => {
- if (cache[index]) {
- return cache[index];
- }
- cache[index] = timeAgoLocale[index] && timeAgoLocale[index]();
- return cache[index];
- };
- };
-
- timeago.register(timeagoLanguageCode, memoizedLocale());
- timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
-
- timeagoInstance = timeago();
- }
+const memoizedLocaleRemaining = () => {
+ const cache = [];
+
+ const timeAgoLocaleRemaining = [
+ () => [s__('Timeago|just now'), s__('Timeago|right now')],
+ () => [s__('Timeago|just now'), s__('Timeago|%s seconds remaining')],
+ () => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
+ () => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')],
+ () => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')],
+ () => [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')],
+ () => [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')],
+ () => [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')],
+ () => [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')],
+ () => [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')],
+ () => [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')],
+ () => [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')],
+ () => [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
+ () => [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
+ ];
+
+ return (number, index) => {
+ if (cache[index]) {
+ return cache[index];
+ }
+ cache[index] = timeAgoLocaleRemaining[index] && timeAgoLocaleRemaining[index]();
+ return cache[index];
+ };
+};
+
+const memoizedLocale = () => {
+ const cache = [];
+
+ const timeAgoLocale = [
+ () => [s__('Timeago|just now'), s__('Timeago|right now')],
+ () => [s__('Timeago|just now'), s__('Timeago|in %s seconds')],
+ () => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
+ () => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')],
+ () => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')],
+ () => [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')],
+ () => [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')],
+ () => [s__('Timeago|%s days ago'), s__('Timeago|in %s days')],
+ () => [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')],
+ () => [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')],
+ () => [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')],
+ () => [s__('Timeago|%s months ago'), s__('Timeago|in %s months')],
+ () => [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
+ () => [s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
+ ];
- return timeagoInstance;
+ return (number, index) => {
+ if (cache[index]) {
+ return cache[index];
+ }
+ cache[index] = timeAgoLocale[index] && timeAgoLocale[index]();
+ return cache[index];
+ };
};
+timeago.register(timeagoLanguageCode, memoizedLocale());
+timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
+
+export const getTimeago = () => timeago;
+
/**
* For the given elements, sets a tooltip with a formatted date.
* @param {JQuery} $timeagoEls
* @param {Boolean} setTimeago
*/
export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
- getTimeago();
-
$timeagoEls.each((i, el) => {
- $(el).text(timeagoInstance.format($(el).attr('datetime'), timeagoLanguageCode));
+ $(el).text(timeago.format($(el).attr('datetime'), timeagoLanguageCode));
});
if (!setTimeago) {
@@ -207,9 +197,7 @@ export const timeFor = (time, expiredLabel) => {
if (new Date(time) < new Date()) {
return expiredLabel || s__('Timeago|Past due');
}
- return getTimeago()
- .format(time, `${timeagoLanguageCode}-remaining`)
- .trim();
+ return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim();
};
export const getDayDifference = (a, b) => {
@@ -459,7 +447,7 @@ export const parsePikadayDate = dateString => {
/**
* Used `onSelect` method in pickaday
* @param {Date} date UTC format
- * @return {String} Date formated in yyyy-mm-dd
+ * @return {String} Date formatted in yyyy-mm-dd
*/
export const pikadayToString = date => {
const day = pad(date.getDate());
@@ -525,8 +513,8 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => {
if (fullNameFormat && isNonZero) {
// Remove traling 's' if unit value is singular
- const formatedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
- return `${memo} ${unitValue} ${formatedUnitName}`;
+ const formattedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
+ return `${memo} ${unitValue} ${formattedUnitName}`;
}
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
@@ -602,3 +590,19 @@ export const getDatesInRange = (d1, d2, formatter = x => x) => {
* @return {Number} number of milliseconds
*/
export const secondsToMilliseconds = seconds => seconds * 1000;
+
+/**
+ * Converts the supplied number of seconds to days.
+ *
+ * @param {Number} seconds
+ * @return {Number} number of days
+ */
+export const secondsToDays = seconds => Math.round(seconds / 86400);
+
+/**
+ * Returns the date after the date provided
+ *
+ * @param {Date} date the initial date
+ * @return {Date} the date following the date provided
+ */
+export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() + 1));
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index 5e5d10883a3..1c7d59054dc 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -21,6 +21,7 @@ const httpStatusCodes = {
NOT_FOUND: 404,
GONE: 410,
UNPROCESSABLE_ENTITY: 422,
+ SERVICE_UNAVAILABLE: 503,
};
export const successCodes = [
diff --git a/app/assets/javascripts/lib/utils/logoutput_behaviours.js b/app/assets/javascripts/lib/utils/logoutput_behaviours.js
deleted file mode 100644
index 41b57025cc9..00000000000
--- a/app/assets/javascripts/lib/utils/logoutput_behaviours.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import $ from 'jquery';
-import {
- canScroll,
- isScrolledToBottom,
- isScrolledToTop,
- isScrolledToMiddle,
- toggleDisableButton,
-} from './scroll_utils';
-
-export default class LogOutputBehaviours {
- constructor() {
- // Scroll buttons
- this.$scrollTopBtn = $('.js-scroll-up');
- this.$scrollBottomBtn = $('.js-scroll-down');
-
- this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this));
- this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this));
- }
-
- toggleScroll() {
- if (canScroll()) {
- if (isScrolledToMiddle()) {
- // User is in the middle of the log
-
- toggleDisableButton(this.$scrollTopBtn, false);
- toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (isScrolledToTop()) {
- // User is at Top of Log
-
- toggleDisableButton(this.$scrollTopBtn, true);
- toggleDisableButton(this.$scrollBottomBtn, false);
- } else if (isScrolledToBottom()) {
- // User is at the bottom of the build log.
-
- toggleDisableButton(this.$scrollTopBtn, false);
- toggleDisableButton(this.$scrollBottomBtn, true);
- }
- } else {
- toggleDisableButton(this.$scrollTopBtn, true);
- toggleDisableButton(this.$scrollBottomBtn, true);
- }
- }
-
- toggleScrollAnimation(toggle) {
- this.$scrollBottomBtn.toggleClass('animate', toggle);
- }
-}
diff --git a/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js b/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js
index 4c61da9b862..fb4d9b7de9c 100644
--- a/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js
+++ b/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js
@@ -2,8 +2,8 @@
* An Axios error interceptor that suppresses AJAX errors caused
* by the request being cancelled when the user navigates to a new page
*/
-export default (err, isUserNavigating, featureFlagEnabled) => {
- if (featureFlagEnabled && isUserNavigating && err.code === 'ECONNABORTED') {
+export default (err, isUserNavigating) => {
+ if (isUserNavigating && err.code === 'ECONNABORTED') {
// If the user is navigating away from the current page,
// prevent .then() and .catch() handlers from being
// called by returning a Promise that never resolves
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 2e0270ee42f..cccf9ad311c 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, no-var, no-param-reassign, one-var, operator-assignment, no-else-return, consistent-return */
+/* eslint-disable func-names, no-param-reassign, operator-assignment, no-else-return, consistent-return */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
@@ -13,8 +13,7 @@ function addBlockTags(blockTag, selected) {
}
function lineBefore(text, textarea) {
- var split;
- split = text
+ const split = text
.substring(0, textarea.selectionStart)
.trim()
.split('\n');
@@ -80,7 +79,7 @@ function moveCursor({
editorSelectionStart,
editorSelectionEnd,
}) {
- var pos;
+ let pos;
if (textArea && !textArea.setSelectionRange) {
return;
}
@@ -132,18 +131,13 @@ export function insertMarkdownText({
select,
editor,
}) {
- var textToInsert,
- selectedSplit,
- startChar,
- removedLastNewLine,
- removedFirstNewLine,
- currentLineEmpty,
- lastNewLine,
- editorSelectionStart,
- editorSelectionEnd;
- removedLastNewLine = false;
- removedFirstNewLine = false;
- currentLineEmpty = false;
+ let removedLastNewLine = false;
+ let removedFirstNewLine = false;
+ let currentLineEmpty = false;
+ let editorSelectionStart;
+ let editorSelectionEnd;
+ let lastNewLine;
+ let textToInsert;
if (editor) {
const selectionRange = editor.getSelectionRange();
@@ -186,7 +180,7 @@ export function insertMarkdownText({
}
}
- selectedSplit = selected.split('\n');
+ const selectedSplit = selected.split('\n');
if (editor && !wrap) {
lastNewLine = editor.getValue().split('\n')[editorSelectionStart.row];
@@ -207,8 +201,7 @@ export function insertMarkdownText({
(textArea && textArea.selectionStart === 0) ||
(editor && editorSelectionStart.column === 0 && editorSelectionStart.row === 0);
- startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : '';
-
+ const startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : '';
const textPlaceholder = '{text}';
if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) {
@@ -263,11 +256,10 @@ export function insertMarkdownText({
}
function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagContent }) {
- var $textArea, selected, text;
- $textArea = $(textArea);
+ const $textArea = $(textArea);
textArea = $textArea.get(0);
- text = $textArea.val();
- selected = selectedText(text, textArea) || tagContent;
+ const text = $textArea.val();
+ const selected = selectedText(text, textArea) || tagContent;
$textArea.focus();
return insertMarkdownText({
textArea,
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 0c194d67bce..6bbf118d7d1 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -72,7 +72,7 @@ export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3
* @param {String} sha
* @returns {String}
*/
-export const truncateSha = sha => sha.substr(0, 8);
+export const truncateSha = sha => sha.substring(0, 8);
const ELLIPSIS_CHAR = '…';
export const truncatePathMiddleToLength = (text, maxWidth) => {
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 4be0d05a9b7..d48678c21f6 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,4 +1,6 @@
-import { join as joinPaths } from 'path';
+const PATH_SEPARATOR = '/';
+const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`);
+const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`);
// Returns a decoded url parameter value
// - Treats '+' as '%20'
@@ -6,6 +8,37 @@ function decodeUrlParameter(val) {
return decodeURIComponent(val.replace(/\+/g, '%20'));
}
+function cleanLeadingSeparator(path) {
+ return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
+}
+
+function cleanEndingSeparator(path) {
+ return path.replace(PATH_SEPARATOR_ENDING_REGEX, '');
+}
+
+/**
+ * Safely joins the given paths which might both start and end with a `/`
+ *
+ * Example:
+ * - `joinPaths('abc/', '/def') === 'abc/def'`
+ * - `joinPaths(null, 'abc/def', 'zoo) === 'abc/def/zoo'`
+ *
+ * @param {...String} paths
+ * @returns {String}
+ */
+export function joinPaths(...paths) {
+ return paths.reduce((acc, path) => {
+ if (!path) {
+ return acc;
+ }
+ if (!acc) {
+ return path;
+ }
+
+ return [cleanEndingSeparator(acc), PATH_SEPARATOR, cleanLeadingSeparator(path)].join('');
+ }, '');
+}
+
// Returns an array containing the value(s) of the
// of the key passed as an argument
export function getParameterValues(sParam, url = window.location) {
@@ -181,4 +214,71 @@ export function getWebSocketUrl(path) {
return `${getWebSocketProtocol()}//${joinPaths(window.location.host, path)}`;
}
-export { joinPaths };
+/**
+ * Convert search query into an object
+ *
+ * @param {String} query from "document.location.search"
+ * @returns {Object}
+ *
+ * ex: "?one=1&two=2" into {one: 1, two: 2}
+ */
+export function queryToObject(query) {
+ const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query;
+ return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => {
+ const p = curr.split('=');
+ accumulator[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
+ return accumulator;
+ }, {});
+}
+
+/**
+ * Convert search query object back into a search query
+ *
+ * @param {Object} obj that needs to be converted
+ * @returns {String}
+ *
+ * ex: {one: 1, two: 2} into "one=1&two=2"
+ *
+ */
+export function objectToQuery(obj) {
+ return Object.keys(obj)
+ .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`)
+ .join('&');
+}
+
+/**
+ * Sets query params for a given URL
+ * It adds new query params, updates existing params with a new value and removes params with value null/undefined
+ *
+ * @param {Object} params The query params to be set/updated
+ * @param {String} url The url to be operated on
+ * @param {Boolean} clearParams Indicates whether existing query params should be removed or not
+ * @returns {String} A copy of the original with the updated query params
+ */
+export const setUrlParams = (params, url = window.location.href, clearParams = false) => {
+ const urlObj = new URL(url);
+ const queryString = urlObj.search;
+ const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString);
+
+ Object.keys(params).forEach(key => {
+ if (params[key] === null || params[key] === undefined) {
+ searchParams.delete(key);
+ } else if (Array.isArray(params[key])) {
+ params[key].forEach((val, idx) => {
+ if (idx === 0) {
+ searchParams.set(key, val);
+ } else {
+ searchParams.append(key, val);
+ }
+ });
+ } else {
+ searchParams.set(key, params[key]);
+ }
+ });
+
+ urlObj.search = searchParams.toString();
+
+ return urlObj.toString();
+};
+
+export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/');