debian-mirror-gitlab/app/assets/javascripts/lib/utils/common_utils.js

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

781 lines
25 KiB
JavaScript
Raw Normal View History

2019-03-02 22:35:43 +05:30
/**
* @module common-utils
*/
2020-03-13 15:44:24 +05:30
import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils';
2018-05-09 12:01:36 +05:30
import $ from 'jquery';
2021-03-11 19:13:27 +05:30
import { isFunction, defer } from 'lodash';
2022-07-16 23:28:13 +05:30
import Cookies from '~/lib/utils/cookies';
2021-11-18 22:05:49 +05:30
import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
2020-01-01 13:55:28 +05:30
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
2018-11-08 19:23:39 +05:30
import { isObject } from './type_utility';
2021-03-11 19:13:27 +05:30
import { getLocationHash } from './url_utility';
2018-03-17 18:26:18 +05:30
2022-08-13 15:12:31 +05:30
export const NO_SCROLL_TO_HASH_CLASS = 'js-no-scroll-to-hash';
2018-11-08 19:23:39 +05:30
export const getPagePath = (index = 0) => {
2022-07-16 23:28:13 +05:30
const { page = '' } = document.body.dataset;
2018-11-08 19:23:39 +05:30
return page.split(':')[index];
};
2018-03-17 18:26:18 +05:30
2018-03-27 19:54:05 +05:30
export const checkPageAndAction = (page, action) => {
const pagePath = getPagePath(1);
const actionPath = getPagePath(2);
2018-03-17 18:26:18 +05:30
2018-03-27 19:54:05 +05:30
return pagePath === page && actionPath === action;
2018-03-17 18:26:18 +05:30
};
2021-01-03 14:25:43 +05:30
export const isInIncidentPage = () => checkPageAndAction('incidents', 'show');
2018-03-27 19:54:05 +05:30
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
2021-03-11 19:13:27 +05:30
export const isInDesignPage = () => checkPageAndAction('issues', 'designs');
2018-03-27 19:54:05 +05:30
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
2018-05-09 12:01:36 +05:30
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
2018-03-17 18:26:18 +05:30
2021-09-30 23:02:18 +05:30
export const getDashPath = (path = window.location.pathname) => path.split('/-/')[1] || null;
2019-10-12 21:52:04 +05:30
export const getCspNonceValue = () => {
const metaTag = document.querySelector('meta[name=csp-nonce]');
return metaTag && metaTag.content;
};
2021-03-08 18:12:59 +05:30
export const rstrip = (val) => {
2018-03-17 18:26:18 +05:30
if (val) {
return val.replace(/\s+$/, '');
}
return val;
};
export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => {
const field = $(fieldSelector);
const closestSubmit = field.closest('form').find(buttonSelector);
if (rstrip(field.val()) === '') {
closestSubmit.disable();
}
// eslint-disable-next-line func-names
2021-03-08 18:12:59 +05:30
return field.on(eventName, function () {
2018-03-17 18:26:18 +05:30
if (rstrip($(this).val()) === '') {
return closestSubmit.disable();
}
return closestSubmit.enable();
});
};
// automatically adjust scroll position for hash urls taking the height of the navbar into account
// https://github.com/twitter/bootstrap/issues/1768
export const handleLocationHash = () => {
let hash = getLocationHash();
if (!hash) return;
// This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash);
const target = document.getElementById(hash) || document.getElementById(`user-content-${hash}`);
2022-08-13 15:12:31 +05:30
// Allow targets to opt out of scroll behavior
if (target?.classList.contains(NO_SCROLL_TO_HASH_CLASS)) return;
2018-03-17 18:26:18 +05:30
const fixedTabs = document.querySelector('.js-tabs-affix');
2018-11-08 19:23:39 +05:30
const fixedDiffStats = document.querySelector('.js-diff-files-changed');
2018-03-17 18:26:18 +05:30
const fixedNav = document.querySelector('.navbar-gitlab');
2018-11-20 20:47:30 +05:30
const performanceBar = document.querySelector('#js-peek');
2018-12-05 23:21:45 +05:30
const topPadding = 8;
2019-09-04 21:01:54 +05:30
const diffFileHeader = document.querySelector('.js-file-title');
const versionMenusContainer = document.querySelector('.mr-version-menus-container');
2020-07-28 23:09:34 +05:30
const fixedIssuableTitle = document.querySelector('.issue-sticky-header');
2018-03-17 18:26:18 +05:30
let adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
if (target && target.scrollIntoView) {
target.scrollIntoView(true);
}
if (fixedTabs) {
adjustment -= fixedTabs.offsetHeight;
}
if (fixedDiffStats) {
adjustment -= fixedDiffStats.offsetHeight;
}
2018-11-20 20:47:30 +05:30
if (performanceBar) {
adjustment -= performanceBar.offsetHeight;
}
2019-09-04 21:01:54 +05:30
if (diffFileHeader) {
adjustment -= diffFileHeader.offsetHeight;
}
if (versionMenusContainer) {
adjustment -= versionMenusContainer.offsetHeight;
}
2020-07-28 23:09:34 +05:30
if (isInIssuePage()) {
2022-07-16 23:28:13 +05:30
adjustment -= fixedIssuableTitle.offsetHeight;
2020-07-28 23:09:34 +05:30
}
2018-12-05 23:21:45 +05:30
if (isInMRPage()) {
adjustment -= topPadding;
}
2020-03-13 15:44:24 +05:30
setTimeout(() => {
window.scrollBy(0, adjustment);
});
2018-03-17 18:26:18 +05:30
};
// Check if element scrolled into viewport from above or below
2019-03-02 22:35:43 +05:30
export const isInViewport = (el, offset = {}) => {
2018-03-17 18:26:18 +05:30
const rect = el.getBoundingClientRect();
2019-03-02 22:35:43 +05:30
const { top, left } = offset;
2018-03-17 18:26:18 +05:30
return (
2019-03-02 22:35:43 +05:30
rect.top >= (top || 0) &&
rect.left >= (left || 0) &&
2018-03-17 18:26:18 +05:30
rect.bottom <= window.innerHeight &&
2019-07-07 11:18:12 +05:30
parseInt(rect.right, 10) <= window.innerWidth
2018-03-17 18:26:18 +05:30
);
};
2021-03-08 18:12:59 +05:30
export const isMetaKey = (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
2018-03-17 18:26:18 +05:30
// Identify following special clicks
// 1) Cmd + Click on Mac (e.metaKey)
// 2) Ctrl + Click on PC (e.ctrlKey)
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
2021-03-08 18:12:59 +05:30
export const isMetaClick = (e) => e.metaKey || e.ctrlKey || e.which === 2;
2018-03-17 18:26:18 +05:30
2021-10-27 15:23:28 +05:30
/**
* Get the current computed outer height for given selector.
*/
export const getOuterHeight = (selector) => {
const element = document.querySelector(selector);
if (!element) {
return undefined;
}
return element.offsetHeight;
};
2018-11-08 19:23:39 +05:30
export const contentTop = () => {
2019-07-07 11:18:12 +05:30
const isDesktop = breakpointInstance.isDesktop();
2021-02-22 17:27:13 +05:30
const heightCalculators = [
2021-10-27 15:23:28 +05:30
() => getOuterHeight('#js-peek'),
() => getOuterHeight('.navbar-gitlab'),
2021-02-22 17:27:13 +05:30
({ desktop }) => {
2022-07-16 23:28:13 +05:30
const container = document.querySelector('.discussions-counter');
2021-02-22 17:27:13 +05:30
let size = 0;
if (!desktop && container) {
size = container.offsetHeight;
}
2018-11-08 19:23:39 +05:30
2021-02-22 17:27:13 +05:30
return size;
},
2021-10-27 15:23:28 +05:30
() => getOuterHeight('.merge-request-tabs'),
() => getOuterHeight('.js-diff-files-changed'),
2022-03-02 08:16:31 +05:30
() => getOuterHeight('.issue-sticky-header.gl-fixed'),
2021-02-22 17:27:13 +05:30
({ desktop }) => {
const diffsTabIsActive = window.mrTabs?.currentAction === 'diffs';
let size;
if (desktop && diffsTabIsActive) {
2021-10-27 15:23:28 +05:30
size = getOuterHeight('.diff-file .file-title-flex-parent:not([style="display:none"])');
2021-02-22 17:27:13 +05:30
}
return size;
},
({ desktop }) => {
let size;
if (desktop) {
2021-10-27 15:23:28 +05:30
size = getOuterHeight('.mr-version-controls');
2021-02-22 17:27:13 +05:30
}
return size;
},
];
return heightCalculators.reduce((totalHeight, calculator) => {
return totalHeight + (calculator({ desktop: isDesktop }) || 0);
}, 0);
2018-11-08 19:23:39 +05:30
};
2020-06-23 00:09:42 +05:30
export const scrollToElement = (element, options = {}) => {
2021-03-08 18:12:59 +05:30
let el = element;
if (element instanceof $) {
// eslint-disable-next-line prefer-destructuring
el = element[0];
} else if (typeof el === 'string') {
el = document.querySelector(element);
2018-03-27 19:54:05 +05:30
}
2018-03-17 18:26:18 +05:30
2021-03-08 18:12:59 +05:30
if (el && el.getBoundingClientRect) {
// In the previous implementation, jQuery naturally deferred this scrolling.
// Unfortunately, we're quite coupled to this implementation detail now.
defer(() => {
2021-12-11 22:18:48 +05:30
const { duration = 200, offset = 0, behavior = duration ? 'smooth' : 'auto' } = options;
2021-03-08 18:12:59 +05:30
const y = el.getBoundingClientRect().top + window.pageYOffset + offset - contentTop();
2021-12-11 22:18:48 +05:30
window.scrollTo({ top: y, behavior });
2021-03-08 18:12:59 +05:30
});
}
2018-03-17 18:26:18 +05:30
};
2021-12-11 22:18:48 +05:30
export const scrollToElementWithContext = (element, options) => {
2020-06-23 00:09:42 +05:30
const offsetMultiplier = -0.1;
2021-12-11 22:18:48 +05:30
return scrollToElement(element, { ...options, offset: window.innerHeight * offsetMultiplier });
2020-06-23 00:09:42 +05:30
};
2019-03-02 22:35:43 +05:30
/**
* Returns a function that can only be invoked once between
* each browser screen repaint.
* @param {Function} fn
*/
2021-03-08 18:12:59 +05:30
export const debounceByAnimationFrame = (fn) => {
2019-03-02 22:35:43 +05:30
let requestId;
return function debounced(...args) {
if (requestId) {
window.cancelAnimationFrame(requestId);
}
requestId = window.requestAnimationFrame(() => fn.apply(this, args));
};
};
2019-02-15 15:39:39 +05:30
const handleSelectedRange = (range, restrictToNode) => {
// Make sure this range is within the restricting container
if (restrictToNode && !range.intersectsNode(restrictToNode)) return null;
// If only a part of the range is within the wanted container, we need to restrict the range to it
if (restrictToNode && !restrictToNode.contains(range.commonAncestorContainer)) {
if (!restrictToNode.contains(range.startContainer)) range.setStart(restrictToNode, 0);
if (!restrictToNode.contains(range.endContainer))
range.setEnd(restrictToNode, restrictToNode.childNodes.length);
}
2018-11-08 19:23:39 +05:30
const container = range.commonAncestorContainer;
// add context to fragment if needed
if (container.tagName === 'OL') {
const parentContainer = document.createElement(container.tagName);
parentContainer.appendChild(range.cloneContents());
return parentContainer;
}
return range.cloneContents();
};
2021-03-08 18:12:59 +05:30
export const getSelectedFragment = (restrictToNode) => {
2018-03-17 18:26:18 +05:30
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
2019-02-15 15:39:39 +05:30
// Most usages of the selection only want text from a part of the page (e.g. discussion)
if (restrictToNode && !selection.containsNode(restrictToNode, true)) return null;
2018-03-17 18:26:18 +05:30
const documentFragment = document.createDocumentFragment();
2019-02-15 15:39:39 +05:30
documentFragment.originalNodes = [];
2018-11-08 19:23:39 +05:30
2018-03-17 18:26:18 +05:30
for (let i = 0; i < selection.rangeCount; i += 1) {
2018-11-08 19:23:39 +05:30
const range = selection.getRangeAt(i);
2019-02-15 15:39:39 +05:30
const handledRange = handleSelectedRange(range, restrictToNode);
if (handledRange) {
documentFragment.appendChild(handledRange);
documentFragment.originalNodes.push(range.commonAncestorContainer);
}
2018-03-17 18:26:18 +05:30
}
2020-03-13 15:44:24 +05:30
if (documentFragment.textContent.length === 0 && documentFragment.children.length === 0) {
return null;
}
2018-03-17 18:26:18 +05:30
return documentFragment;
};
2022-07-16 23:28:13 +05:30
function execInsertText(text) {
if (text === '') return document.execCommand('delete');
return document.execCommand('insertText', false, text);
}
/**
* This method inserts text into a textarea/input field.
* Uses `execCommand` if supported
*
* @param {HTMLElement} target - textarea/input to have text inserted into
* @param {String | function} text - text to be inserted
*/
2018-03-17 18:26:18 +05:30
export const insertText = (target, text) => {
2018-11-08 19:23:39 +05:30
const { selectionStart, selectionEnd, value } = target;
2018-03-17 18:26:18 +05:30
const textBefore = value.substring(0, selectionStart);
const textAfter = value.substring(selectionEnd, value.length);
const insertedText = text instanceof Function ? text(textBefore, textAfter) : text;
2022-07-16 23:28:13 +05:30
// The `execCommand` is officially deprecated. However, for `insertText`,
// there is currently no alternative. We need to use it in order to trigger
// the browser's undo tracking when we insert text.
// Per https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand on 2022-04-11,
// The Clipboard API can be used instead of execCommand in many cases,
// but execCommand is still sometimes useful. In particular, the Clipboard
// API doesn't replace the insertText command
// So we attempt to use it if possible. Otherwise, fall back to just replacing
// the value as before. In this case, Undo will be broken with inserted text.
// Testing on older versions of Firefox:
// 87 and below: does not work and falls through to just replacing value.
// 87 was released in Mar of 2021
// 89 and above: works well
// 89 was released in May of 2021
if (!execInsertText(insertedText)) {
const newText = textBefore + insertedText + textAfter;
// eslint-disable-next-line no-param-reassign
target.value = newText;
// eslint-disable-next-line no-param-reassign
target.selectionStart = selectionStart + insertedText.length;
// eslint-disable-next-line no-param-reassign
target.selectionEnd = selectionStart + insertedText.length;
}
2018-03-17 18:26:18 +05:30
// Trigger autosave
target.dispatchEvent(new Event('input'));
// Trigger autosize
const event = document.createEvent('Event');
event.initEvent('autosize:update', true, false);
target.dispatchEvent(event);
};
/**
2021-09-30 23:02:18 +05:30
this will take in the headers from an API response and normalize them
this way we don't run into production issues when nginx gives us lowercased header keys
2018-03-17 18:26:18 +05:30
*/
2021-03-08 18:12:59 +05:30
export const normalizeHeaders = (headers) => {
2018-03-17 18:26:18 +05:30
const upperCaseHeaders = {};
2021-03-08 18:12:59 +05:30
Object.keys(headers || {}).forEach((e) => {
2018-03-17 18:26:18 +05:30
upperCaseHeaders[e.toUpperCase()] = headers[e];
});
return upperCaseHeaders;
};
/**
* Parses pagination object string values into numbers.
*
* @param {Object} paginationInformation
* @returns {Object}
*/
2021-03-08 18:12:59 +05:30
export const parseIntPagination = (paginationInformation) => ({
2018-03-17 18:26:18 +05:30
perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
page: parseInt(paginationInformation['X-PAGE'], 10),
total: parseInt(paginationInformation['X-TOTAL'], 10),
totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
});
2021-03-08 18:12:59 +05:30
export const buildUrlWithCurrentLocation = (param) => {
2018-12-05 23:21:45 +05:30
if (param) return `${window.location.pathname}${param}`;
return window.location.pathname;
};
2018-03-17 18:26:18 +05:30
/**
* Based on the current location and the string parameters provided
* creates a new entry in the history without reloading the page.
*
* @param {String} param
*/
2021-03-08 18:12:59 +05:30
export const historyPushState = (newUrl) => {
2018-03-17 18:26:18 +05:30
window.history.pushState({}, document.title, newUrl);
};
2020-03-13 15:44:24 +05:30
/**
* Based on the current location and the string parameters provided
* overwrites the current entry in the history without reloading the page.
*
* @param {String} param
*/
2021-03-08 18:12:59 +05:30
export const historyReplaceState = (newUrl) => {
2020-03-13 15:44:24 +05:30
window.history.replaceState({}, document.title, newUrl);
};
2019-02-15 15:39:39 +05:30
/**
2019-03-02 22:35:43 +05:30
* Returns true for a String value of "true" and false otherwise.
* This is the opposite of Boolean(...).toString().
* `parseBoolean` is idempotent.
2019-02-15 15:39:39 +05:30
*
* @param {String} value
* @returns {Boolean}
*/
2021-03-08 18:12:59 +05:30
export const parseBoolean = (value) => (value && value.toString()) === 'true';
2019-02-15 15:39:39 +05:30
2020-01-01 13:55:28 +05:30
export const BACKOFF_TIMEOUT = 'BACKOFF_TIMEOUT';
2019-03-02 22:35:43 +05:30
/**
* @callback backOffCallback
* @param {Function} next
* @param {Function} stop
*/
2018-03-17 18:26:18 +05:30
/**
* Back Off exponential algorithm
* backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
*
2019-03-02 22:35:43 +05:30
* @param {backOffCallback} fn function to be called
2018-03-17 18:26:18 +05:30
* @param {Number} timeout
* @return {Promise<Any, Error>}
* @example
* ```
* backOff(function (next, stop) {
* // Let's perform this function repeatedly for 60s or for the timeout provided.
*
* ourFunction()
* .then(function (result) {
* // continue if result is not what we need
* next();
*
* // when result is what we need let's stop with the repetions and jump out of the cycle
* stop(result);
* })
* .catch(function (error) {
* // if there is an error, we need to stop this with an error.
* stop(error);
* })
* }, 60000)
* .then(function (result) {})
* .catch(function (error) {
* // deal with errors passed to stop()
* })
* ```
*/
export const backOff = (fn, timeout = 60000) => {
const maxInterval = 32000;
let nextInterval = 2000;
let timeElapsed = 0;
return new Promise((resolve, reject) => {
2021-03-08 18:12:59 +05:30
const stop = (arg) => (arg instanceof Error ? reject(arg) : resolve(arg));
2018-03-17 18:26:18 +05:30
const next = () => {
if (timeElapsed < timeout) {
setTimeout(() => fn(next, stop), nextInterval);
timeElapsed += nextInterval;
nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
2017-08-17 22:00:37 +05:30
} else {
2020-01-01 13:55:28 +05:30
reject(new Error(BACKOFF_TIMEOUT));
2017-08-17 22:00:37 +05:30
}
};
2018-03-17 18:26:18 +05:30
fn(next, stop);
});
};
export const spriteIcon = (icon, className = '') => {
const classAttribute = className.length > 0 ? `class="${className}"` : '';
return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
};
/**
2020-04-08 14:13:33 +05:30
* @callback ConversionFunction
* @param {string} prop
*/
/**
* This function takes a conversion function as the first parameter
* and applies this function to each prop in the provided object.
2019-03-02 22:35:43 +05:30
*
* This method also supports additional params in `options` object
*
2020-04-08 14:13:33 +05:30
* @param {ConversionFunction} conversionFunction - Function to apply to each prop of the object.
2019-03-02 22:35:43 +05:30
* @param {Object} obj - Object to be converted.
* @param {Object} options - Object containing additional options.
* @param {boolean} options.deep - FLag to allow deep object converting
2020-04-08 14:13:33 +05:30
* @param {Array[]} options.dropKeys - List of properties to discard while building new object
* @param {Array[]} options.ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
2018-03-17 18:26:18 +05:30
*/
2020-04-08 14:13:33 +05:30
export const convertObjectProps = (conversionFunction, obj = {}, options = {}) => {
if (!isFunction(conversionFunction) || obj === null) {
2018-03-17 18:26:18 +05:30
return {};
}
2019-03-02 22:35:43 +05:30
const { deep = false, dropKeys = [], ignoreKeyNames = [] } = options;
2018-11-08 19:23:39 +05:30
2020-04-08 14:13:33 +05:30
const isObjParameterArray = Array.isArray(obj);
const initialValue = isObjParameterArray ? [] : {};
2018-03-17 18:26:18 +05:30
return Object.keys(obj).reduce((acc, prop) => {
2018-11-08 19:23:39 +05:30
const val = obj[prop];
2018-03-17 18:26:18 +05:30
2019-03-02 22:35:43 +05:30
// Drop properties from new object if
// there are any mentioned in options
if (dropKeys.indexOf(prop) > -1) {
return acc;
}
// Skip converting properties in new object
// if there are any mentioned in options
if (ignoreKeyNames.indexOf(prop) > -1) {
2020-04-08 14:13:33 +05:30
acc[prop] = val;
2019-03-02 22:35:43 +05:30
return acc;
}
if (deep && (isObject(val) || Array.isArray(val))) {
2020-04-08 14:13:33 +05:30
if (isObjParameterArray) {
acc[prop] = convertObjectProps(conversionFunction, val, options);
} else {
acc[conversionFunction(prop)] = convertObjectProps(conversionFunction, val, options);
}
2020-06-23 00:09:42 +05:30
} else if (isObjParameterArray) {
acc[prop] = val;
2018-11-08 19:23:39 +05:30
} else {
2020-04-08 14:13:33 +05:30
acc[conversionFunction(prop)] = val;
2018-11-08 19:23:39 +05:30
}
2018-03-17 18:26:18 +05:30
return acc;
2020-04-08 14:13:33 +05:30
}, initialValue);
2018-03-17 18:26:18 +05:30
};
2020-04-08 14:13:33 +05:30
/**
* This method takes in object with snake_case property names
* and returns a new object with camelCase property names
*
* Reasoning for this method is to ensure consistent property
* naming conventions across JS code.
*
* This method also supports additional params in `options` object
*
* @param {Object} obj - Object to be converted.
* @param {Object} options - Object containing additional options.
* @param {boolean} options.deep - FLag to allow deep object converting
* @param {Array[]} options.dropKeys - List of properties to discard while building new object
* @param {Array[]} options.ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
*/
export const convertObjectPropsToCamelCase = (obj = {}, options = {}) =>
convertObjectProps(convertToCamelCase, obj, options);
2020-01-01 13:55:28 +05:30
/**
* Converts all the object keys to snake case
*
2020-04-08 14:13:33 +05:30
* This method also supports additional params in `options` object
*
* @param {Object} obj - Object to be converted.
* @param {Object} options - Object containing additional options.
* @param {boolean} options.deep - FLag to allow deep object converting
* @param {Array[]} options.dropKeys - List of properties to discard while building new object
* @param {Array[]} options.ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object
2020-01-01 13:55:28 +05:30
*/
2020-04-08 14:13:33 +05:30
export const convertObjectPropsToSnakeCase = (obj = {}, options = {}) =>
convertObjectProps(convertToSnakeCase, obj, options);
2020-01-01 13:55:28 +05:30
2018-03-27 19:54:05 +05:30
export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
// Click a .js-select-on-focus field, select the contents
// Prevent a mouseup event from deselecting the input
$(selector).on('focusin', function selectOnFocusCallback() {
2018-11-08 19:23:39 +05:30
$(this)
.select()
2021-03-08 18:12:59 +05:30
.one('mouseup', (e) => {
2018-11-08 19:23:39 +05:30
e.preventDefault();
});
2018-03-27 19:54:05 +05:30
});
};
2018-11-18 11:00:15 +05:30
/**
* Method to round of values with decimal places
* with provided precision.
*
* Eg; roundOffFloat(3.141592, 3) = 3.142
*
2021-11-11 11:23:49 +05:30
* Refer to spec/frontend/lib/utils/common_utils_spec.js for
2018-11-18 11:00:15 +05:30
* more supported examples.
*
* @param {Float} number
* @param {Number} precision
*/
export const roundOffFloat = (number, precision = 0) => {
2022-08-13 15:12:31 +05:30
const multiplier = 10 ** precision;
2018-11-18 11:00:15 +05:30
return Math.round(number * multiplier) / multiplier;
};
2021-04-29 21:17:54 +05:30
/**
* Method to round values to the nearest half (0.5)
*
* Eg; roundToNearestHalf(3.141592) = 3, roundToNearestHalf(3.41592) = 3.5
*
2021-11-11 11:23:49 +05:30
* Refer to spec/frontend/lib/utils/common_utils_spec.js for
2021-04-29 21:17:54 +05:30
* more supported examples.
*
* @param {Float} number
* @returns {Float|Number}
*/
export const roundToNearestHalf = (num) => Math.round(num * 2).toFixed() / 2;
2021-01-29 00:20:46 +05:30
/**
* Method to round down values with decimal places
* with provided precision.
*
* Eg; roundDownFloat(3.141592, 3) = 3.141
*
2021-11-11 11:23:49 +05:30
* Refer to spec/frontend/lib/utils/common_utils_spec.js for
2021-01-29 00:20:46 +05:30
* more supported examples.
*
* @param {Float} number
* @param {Number} precision
*/
export const roundDownFloat = (number, precision = 0) => {
2022-08-13 15:12:31 +05:30
const multiplier = 10 ** precision;
2021-01-29 00:20:46 +05:30
return Math.floor(number * multiplier) / multiplier;
};
2018-12-05 23:21:45 +05:30
/**
* Represents navigation type constants of the Performance Navigation API.
* Detailed explanation see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigation.
*/
export const NavigationType = {
TYPE_NAVIGATE: 0,
TYPE_RELOAD: 1,
TYPE_BACK_FORWARD: 2,
TYPE_RESERVED: 255,
};
2019-12-04 20:38:33 +05:30
/**
* Method to perform case-insensitive search for a string
* within multiple properties and return object containing
* properties in case there are multiple matches or `null`
* if there's no match.
*
* Eg; Suppose we want to allow user to search using for a string
* within `iid`, `title`, `url` or `reference` props of a target object;
*
* const objectToSearch = {
* "iid": 1,
* "title": "Error omnis quos consequatur ullam a vitae sed omnis libero cupiditate. &3",
* "url": "/groups/gitlab-org/-/epics/1",
* "reference": "&1",
* };
*
* Following is how we call searchBy and the return values it will yield;
*
* - `searchBy('omnis', objectToSearch);`: This will return `{ title: ... }` as our
* query was found within title prop we only return that.
* - `searchBy('1', objectToSearch);`: This will return `{ "iid": ..., "reference": ..., "url": ... }`.
* - `searchBy('https://gitlab.com/groups/gitlab-org/-/epics/1', objectToSearch);`:
* This will return `{ "url": ... }`.
* - `searchBy('foo', objectToSearch);`: This will return `null` as no property value
* matched with our query.
*
* You can learn more about behaviour of this method by referring to tests
2021-11-11 11:23:49 +05:30
* within `spec/frontend/lib/utils/common_utils_spec.js`.
2019-12-04 20:38:33 +05:30
*
* @param {string} query String to search for
* @param {object} searchSpace Object containing properties to search in for `query`
*/
export const searchBy = (query = '', searchSpace = {}) => {
const targetKeys = searchSpace !== null ? Object.keys(searchSpace) : [];
if (!query || !targetKeys.length) {
return null;
}
const normalizedQuery = query.toLowerCase();
const matches = targetKeys
2021-03-08 18:12:59 +05:30
.filter((item) => {
2019-12-04 20:38:33 +05:30
const searchItem = `${searchSpace[item]}`.toLowerCase();
return (
searchItem.indexOf(normalizedQuery) > -1 ||
normalizedQuery.indexOf(searchItem) > -1 ||
normalizedQuery === searchItem
);
})
.reduce((acc, prop) => {
const match = acc;
match[prop] = searchSpace[prop];
return acc;
}, {});
return Object.keys(matches).length ? matches : null;
};
2019-07-31 22:56:46 +05:30
/**
* Checks if the given Label has a special syntax `::` in
* it's title.
*
* Expected Label to be an Object with `title` as a key:
* { title: 'LabelTitle', ...otherProperties };
*
* @param {Object} label
* @returns Boolean
*/
2021-11-18 22:05:49 +05:30
export const isScopedLabel = ({ title = '' } = {}) => title.includes(SCOPED_LABEL_DELIMITER);
2021-09-30 23:02:18 +05:30
2021-12-11 22:18:48 +05:30
const scopedLabelRegex = new RegExp(`(.*)${SCOPED_LABEL_DELIMITER}.*`);
2021-09-30 23:02:18 +05:30
/**
2021-12-11 22:18:48 +05:30
* Returns the key of a scoped label.
* For example:
* - returns `scoped` if the label is `scoped::value`.
* - returns `scoped::label` if the label is `scoped::label::value`.
2021-09-30 23:02:18 +05:30
*
2021-12-11 22:18:48 +05:30
* @param {Object} label object containing `title` property
* @returns String scoped label key, or full label if it is not a scoped label
2021-09-30 23:02:18 +05:30
*/
2021-12-11 22:18:48 +05:30
export const scopedLabelKey = ({ title = '' }) => {
return title.replace(scopedLabelRegex, '$1');
};
2019-07-31 22:56:46 +05:30
2020-04-22 19:07:51 +05:30
// Methods to set and get Cookie
2022-04-04 11:22:00 +05:30
export const setCookie = (name, value, attributes) => {
const defaults = { expires: 365, secure: Boolean(window.gon?.secure) };
Cookies.set(name, value, { ...defaults, ...attributes });
};
2020-04-22 19:07:51 +05:30
2021-03-08 18:12:59 +05:30
export const getCookie = (name) => Cookies.get(name);
2020-04-22 19:07:51 +05:30
2021-03-08 18:12:59 +05:30
export const removeCookie = (name) => Cookies.remove(name);
2020-04-22 19:07:51 +05:30
/**
* Returns the status of a feature flag.
* Currently, there is no way to access feature
* flags in Vuex other than directly tapping into
* window.gon.
*
* This should only be used on Vuex. If feature flags
* need to be accessed in Vue components consider
* using the Vue feature flag mixin.
*
* @param {String} flag Feature flag
* @returns {Boolean} on/off
*/
2021-03-08 18:12:59 +05:30
export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag];
2021-03-11 19:13:27 +05:30
/**
* This method takes in array with snake_case strings
* and returns a new array with camelCase strings
*
* @param {Array[String]} array - Array to be converted
* @returns {Array[String]} Converted array
*/
export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i));
2021-09-30 23:02:18 +05:30
export const isLoggedIn = () => Boolean(window.gon?.current_user_id);
2022-01-26 12:08:38 +05:30
/**
* This method takes in array of objects with snake_case
* property names and returns a new array of objects with
* camelCase property names
*
* @param {Array[Object]} array - Array to be converted
* @returns {Array[Object]} Converted array
*/
export const convertArrayOfObjectsToCamelCase = (array) =>
array.map((o) => convertObjectPropsToCamelCase(o));
2022-03-02 08:16:31 +05:30
export const getFirstPropertyValue = (data) => {
if (!data) return null;
const [key] = Object.keys(data);
if (!key) return null;
return data[key];
};