summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib/utils
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 12:26:25 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 12:26:25 +0000
commita09983ae35713f5a2bbb100981116d31ce99826e (patch)
tree2ee2af7bd104d57086db360a7e6d8c9d5d43667a /app/assets/javascripts/lib/utils
parent18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff)
downloadgitlab-ce-a09983ae35713f5a2bbb100981116d31ce99826e.tar.gz
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'app/assets/javascripts/lib/utils')
-rw-r--r--app/assets/javascripts/lib/utils/axios_startup_calls.js52
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js3
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js97
-rw-r--r--app/assets/javascripts/lib/utils/constants.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js7
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js22
-rw-r--r--app/assets/javascripts/lib/utils/grammar.js18
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js43
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js81
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js13
10 files changed, 225 insertions, 113 deletions
diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js
new file mode 100644
index 00000000000..cb2e8a76c08
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js
@@ -0,0 +1,52 @@
+import { isEmpty } from 'lodash';
+import { mergeUrlParams } from './url_utility';
+
+// We should probably not couple this utility to `gon.gitlab_url`
+// Also, this would replace occurrences that aren't at the beginning of the string
+const removeGitLabUrl = url => url.replace(gon.gitlab_url, '');
+
+const getFullUrl = req => {
+ const url = removeGitLabUrl(req.url);
+ return mergeUrlParams(req.params || {}, url);
+};
+
+const setupAxiosStartupCalls = axios => {
+ const { startup_calls: startupCalls } = window.gl || {};
+
+ if (!startupCalls || isEmpty(startupCalls)) {
+ return;
+ }
+
+ // TODO: To save performance of future axios calls, we can
+ // remove this interceptor once the "startupCalls" have been loaded
+ axios.interceptors.request.use(req => {
+ const fullUrl = getFullUrl(req);
+
+ const existing = startupCalls[fullUrl];
+
+ if (existing) {
+ // eslint-disable-next-line no-param-reassign
+ req.adapter = () =>
+ existing.fetchCall.then(res => {
+ const fetchHeaders = {};
+ res.headers.forEach((val, key) => {
+ fetchHeaders[key] = val;
+ });
+
+ // eslint-disable-next-line promise/no-nesting
+ return res.json().then(data => ({
+ data,
+ status: res.status,
+ statusText: res.statusText,
+ headers: fetchHeaders,
+ config: req,
+ request: req,
+ }));
+ });
+ }
+
+ return req;
+ });
+};
+
+export default setupAxiosStartupCalls;
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index 4eec5bffc66..9d517f45caa 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import csrf from './csrf';
import suppressAjaxErrorsDuringNavigation from './suppress_ajax_errors_during_navigation';
+import setupAxiosStartupCalls from './axios_startup_calls';
axios.defaults.headers.common[csrf.headerKey] = csrf.token;
// Used by Rails to check if it is a valid XHR request
@@ -14,6 +15,8 @@ axios.interceptors.request.use(config => {
return config;
});
+setupAxiosStartupCalls(axios);
+
// Remove the global counter
axios.interceptors.response.use(
response => {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index a60748215ab..8bf9a281151 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -53,16 +53,6 @@ export const getCspNonceValue = () => {
return metaTag && metaTag.content;
};
-export const ajaxGet = url =>
- axios
- .get(url, {
- params: { format: 'js' },
- responseType: 'text',
- })
- .then(({ data }) => {
- $.globalEval(data, { nonce: getCspNonceValue() });
- });
-
export const rstrip = val => {
if (val) {
return val.replace(/\s+$/, '');
@@ -105,6 +95,7 @@ export const handleLocationHash = () => {
const topPadding = 8;
const diffFileHeader = document.querySelector('.js-file-title');
const versionMenusContainer = document.querySelector('.mr-version-menus-container');
+ const fixedIssuableTitle = document.querySelector('.issue-sticky-header');
let adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
@@ -133,6 +124,10 @@ export const handleLocationHash = () => {
adjustment -= versionMenusContainer.offsetHeight;
}
+ if (isInIssuePage()) {
+ adjustment -= fixedIssuableTitle.offsetHeight;
+ }
+
if (isInMRPage()) {
adjustment -= topPadding;
}
@@ -370,34 +365,6 @@ export const insertText = (target, text) => {
target.dispatchEvent(event);
};
-export const nodeMatchesSelector = (node, selector) => {
- const matches =
- Element.prototype.matches ||
- Element.prototype.matchesSelector ||
- Element.prototype.mozMatchesSelector ||
- Element.prototype.msMatchesSelector ||
- Element.prototype.oMatchesSelector ||
- Element.prototype.webkitMatchesSelector;
-
- if (matches) {
- return matches.call(node, selector);
- }
-
- // IE11 doesn't support `node.matches(selector)`
-
- let { parentNode } = node;
-
- if (!parentNode) {
- parentNode = document.createElement('div');
- // eslint-disable-next-line no-param-reassign
- node = node.cloneNode(true);
- parentNode.appendChild(node);
- }
-
- const matchingNodes = parentNode.querySelectorAll(selector);
- return Array.prototype.indexOf.call(matchingNodes, node) !== -1;
-};
-
/**
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
@@ -413,24 +380,6 @@ export const normalizeHeaders = headers => {
};
/**
- this will take in the getAllResponseHeaders result and normalize them
- this way we don't run into production issues when nginx gives us lowercased header keys
-*/
-export const normalizeCRLFHeaders = headers => {
- const headersObject = {};
- const headersArray = headers.split('\n');
-
- headersArray.forEach(header => {
- const keyValue = header.split(': ');
-
- // eslint-disable-next-line prefer-destructuring
- headersObject[keyValue[0]] = keyValue[1];
- });
-
- return normalizeHeaders(headersObject);
-};
-
-/**
* Parses pagination object string values into numbers.
*
* @param {Object} paginationInformation
@@ -638,13 +587,6 @@ export const setFaviconOverlay = overlayPath => {
);
};
-export const setFavicon = faviconPath => {
- const faviconEl = document.getElementById('favicon');
- if (faviconEl && faviconPath) {
- faviconEl.setAttribute('href', faviconPath);
- }
-};
-
export const resetFavicon = () => {
const faviconEl = document.getElementById('favicon');
@@ -883,35 +825,6 @@ export const searchBy = (query = '', searchSpace = {}) => {
*/
export const isScopedLabel = ({ title = '' }) => title.indexOf('::') !== -1;
-window.gl = window.gl || {};
-window.gl.utils = {
- ...(window.gl.utils || {}),
- getPagePath,
- isInGroupsPage,
- isInProjectPage,
- getProjectSlug,
- getGroupSlug,
- isInIssuePage,
- ajaxGet,
- rstrip,
- updateTooltipTitle,
- disableButtonIfEmptyField,
- handleLocationHash,
- isInViewport,
- parseUrl,
- parseUrlPathname,
- getUrlParamsArray,
- isMetaKey,
- isMetaClick,
- scrollToElement,
- getParameterByName,
- getSelectedFragment,
- insertText,
- nodeMatchesSelector,
- spriteIcon,
- imagePath,
-};
-
// Methods to set and get Cookie
export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 });
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index eb6c9bf7eb6..993d51370ec 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,5 +1,7 @@
export const BYTES_IN_KIB = 1024;
export const HIDDEN_CLASS = 'hidden';
+export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
+export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
export const DATETIME_RANGE_TYPES = {
fixed: 'fixed',
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 6b69d2febe0..6e02fc1eb91 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -89,13 +89,15 @@ export const getDayName = date =>
* @example
* dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am GMT+0000"
* @param {date} datetime
+ * @param {String} format
+ * @param {Boolean} UTC convert local time to UTC
* @returns {String}
*/
-export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => {
+export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z', utc = false) => {
if (isString(datetime) && datetime.match(/\d+-\d+\d+ /)) {
throw new Error(__('Invalid date'));
}
- return dateFormat(datetime, format);
+ return dateFormat(datetime, format, utc);
};
/**
@@ -425,7 +427,6 @@ export const dayInQuarter = (date, quarter) => {
window.gl = window.gl || {};
window.gl.utils = {
...(window.gl.utils || {}),
- getTimeago,
localTimeAgo,
};
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index 8fa235f8afb..d9b0e8c4476 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -1,3 +1,4 @@
+import { has } from 'lodash';
import { isInIssuePage, isInMRPage, isInEpicPage } from './common_utils';
export const addClassIfElementExists = (element, className) => {
@@ -25,3 +26,24 @@ export const toggleContainerClasses = (containerEl, classList) => {
});
}
};
+
+/**
+ * Return a object mapping element dataset names to booleans.
+ *
+ * This is useful for data- attributes whose presense represent
+ * a truthiness, no matter the value of the attribute. The absense of the
+ * attribute represents falsiness.
+ *
+ * This can be useful when Rails-provided boolean-like values are passed
+ * directly to the HAML template, rather than cast to a string.
+ *
+ * @param {HTMLElement} element - The DOM element to inspect
+ * @param {string[]} names - The dataset (i.e., camelCase) names to inspect
+ * @returns {Object.<string, boolean>}
+ */
+export const parseBooleanDataAttributes = ({ dataset }, names) =>
+ names.reduce((acc, name) => {
+ acc[name] = has(dataset, name);
+
+ return acc;
+ }, {});
diff --git a/app/assets/javascripts/lib/utils/grammar.js b/app/assets/javascripts/lib/utils/grammar.js
index 18f9e2ed846..b1f38429369 100644
--- a/app/assets/javascripts/lib/utils/grammar.js
+++ b/app/assets/javascripts/lib/utils/grammar.js
@@ -20,18 +20,22 @@ export const toNounSeriesText = items => {
if (items.length === 0) {
return '';
} else if (items.length === 1) {
- return items[0];
+ return sprintf(s__(`nounSeries|%{item}`), { item: items[0] }, false);
} else if (items.length === 2) {
- return sprintf(s__('nounSeries|%{firstItem} and %{lastItem}'), {
- firstItem: items[0],
- lastItem: items[1],
- });
+ return sprintf(
+ s__('nounSeries|%{firstItem} and %{lastItem}'),
+ {
+ firstItem: items[0],
+ lastItem: items[1],
+ },
+ false,
+ );
}
return items.reduce((item, nextItem, idx) =>
idx === items.length - 1
- ? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem })
- : sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }),
+ ? sprintf(s__('nounSeries|%{item}, and %{lastItem}'), { item, lastItem: nextItem }, false)
+ : sprintf(s__('nounSeries|%{item}, %{nextItem}'), { item, nextItem }, false),
);
};
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 0dfc144c363..4d25ee9e4bd 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -27,9 +27,28 @@ function lineAfter(text, textarea) {
.split('\n')[0];
}
+function convertMonacoSelectionToAceFormat(sel) {
+ return {
+ start: {
+ row: sel.startLineNumber,
+ column: sel.startColumn,
+ },
+ end: {
+ row: sel.endLineNumber,
+ column: sel.endColumn,
+ },
+ };
+}
+
+function getEditorSelectionRange(editor) {
+ return window.gon.features?.monacoBlobs
+ ? convertMonacoSelectionToAceFormat(editor.getSelection())
+ : editor.getSelectionRange();
+}
+
function editorBlockTagText(text, blockTag, selected, editor) {
const lines = text.split('\n');
- const selectionRange = editor.getSelectionRange();
+ const selectionRange = getEditorSelectionRange(editor);
const shouldRemoveBlock =
lines[selectionRange.start.row - 1] === blockTag &&
lines[selectionRange.end.row + 1] === blockTag;
@@ -90,8 +109,12 @@ function moveCursor({
const endPosition = startPosition + select.length;
return textArea.setSelectionRange(startPosition, endPosition);
} else if (editor) {
- editor.navigateLeft(tag.length - tag.indexOf(select));
- editor.getSelection().selectAWord();
+ if (window.gon.features?.monacoBlobs) {
+ editor.selectWithinSelection(select, tag);
+ } else {
+ editor.navigateLeft(tag.length - tag.indexOf(select));
+ editor.getSelection().selectAWord();
+ }
return;
}
}
@@ -115,7 +138,11 @@ function moveCursor({
}
} else if (editor && editorSelectionStart.row === editorSelectionEnd.row) {
if (positionBetweenTags) {
- editor.navigateLeft(tag.length);
+ if (window.gon.features?.monacoBlobs) {
+ editor.moveCursor(tag.length * -1);
+ } else {
+ editor.navigateLeft(tag.length);
+ }
}
}
}
@@ -140,7 +167,7 @@ export function insertMarkdownText({
let textToInsert;
if (editor) {
- const selectionRange = editor.getSelectionRange();
+ const selectionRange = getEditorSelectionRange(editor);
editorSelectionStart = selectionRange.start;
editorSelectionEnd = selectionRange.end;
@@ -237,7 +264,11 @@ export function insertMarkdownText({
}
if (editor) {
- editor.insert(textToInsert);
+ if (window.gon.features?.monacoBlobs) {
+ editor.replaceSelectedText(textToInsert, select);
+ } else {
+ editor.insert(textToInsert);
+ }
} else {
insertText(textArea, textToInsert);
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index be3fe1ed620..e2953ce330c 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,4 +1,9 @@
-import { isString } from 'lodash';
+import { isString, memoize } from 'lodash';
+
+import {
+ TRUNCATE_WIDTH_DEFAULT_WIDTH,
+ TRUNCATE_WIDTH_DEFAULT_FONT_SIZE,
+} from '~/lib/utils/constants';
/**
* Adds a , to a string composed by numbers, at every 3 chars.
@@ -73,7 +78,79 @@ export const slugifyWithUnderscore = str => slugify(str, '_');
* @param {Number} maxLength
* @returns {String}
*/
-export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3)}...`;
+export const truncate = (string, maxLength) => {
+ if (string.length - 1 > maxLength) {
+ return `${string.substr(0, maxLength - 1)}…`;
+ }
+
+ return string;
+};
+
+/**
+ * This function calculates the average char width. It does so by placing a string in the DOM and measuring the width.
+ * NOTE: This will cause a reflow and should be used sparsely!
+ * The default fontFamily is 'sans-serif' and 12px in ECharts, so that is the default basis for calculating the average with.
+ * https://echarts.apache.org/en/option.html#xAxis.nameTextStyle.fontFamily
+ * https://echarts.apache.org/en/option.html#xAxis.nameTextStyle.fontSize
+ * @param {Object} options
+ * @param {Number} options.fontSize style to size the text for measurement
+ * @param {String} options.fontFamily style of font family to measure the text with
+ * @param {String} options.chars string of chars to use as a basis for calculating average width
+ * @return {Number}
+ */
+const getAverageCharWidth = memoize(function getAverageCharWidth(options = {}) {
+ const {
+ fontSize = 12,
+ fontFamily = 'sans-serif',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ chars = ' ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
+ } = options;
+ const div = document.createElement('div');
+
+ div.style.fontFamily = fontFamily;
+ div.style.fontSize = `${fontSize}px`;
+ // Place outside of view
+ div.style.position = 'absolute';
+ div.style.left = -1000;
+ div.style.top = -1000;
+
+ div.innerHTML = chars;
+
+ document.body.appendChild(div);
+ const width = div.clientWidth;
+ document.body.removeChild(div);
+
+ return width / chars.length / fontSize;
+});
+
+/**
+ * This function returns a truncated version of `string` if its estimated rendered width is longer than `maxWidth`,
+ * otherwise it will return the original `string`
+ * Inspired by https://bl.ocks.org/tophtucker/62f93a4658387bb61e4510c37e2e97cf
+ * @param {String} string text to truncate
+ * @param {Object} options
+ * @param {Number} options.maxWidth largest rendered width the text may have
+ * @param {Number} options.fontSize size of the font used to render the text
+ * @return {String} either the original string or a truncated version
+ */
+export const truncateWidth = (string, options = {}) => {
+ const {
+ maxWidth = TRUNCATE_WIDTH_DEFAULT_WIDTH,
+ fontSize = TRUNCATE_WIDTH_DEFAULT_FONT_SIZE,
+ } = options;
+ const { truncateIndex } = string.split('').reduce(
+ (memo, char, index) => {
+ let newIndex = index;
+ if (memo.width > maxWidth) {
+ newIndex = memo.truncateIndex;
+ }
+ return { width: memo.width + getAverageCharWidth() * fontSize, truncateIndex: newIndex };
+ },
+ { width: 0, truncateIndex: 0 },
+ );
+
+ return truncate(string, truncateIndex);
+};
/**
* Truncate SHA to 8 characters
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 0472b8cf51f..c6c34b831ee 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -344,9 +344,15 @@ export function objectToQuery(obj) {
* @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
+ * @param {Boolean} railsArraySyntax When enabled, changes the array syntax from `keys=` to `keys[]=` according to Rails conventions
* @returns {String} A copy of the original with the updated query params
*/
-export const setUrlParams = (params, url = window.location.href, clearParams = false) => {
+export const setUrlParams = (
+ params,
+ url = window.location.href,
+ clearParams = false,
+ railsArraySyntax = false,
+) => {
const urlObj = new URL(url);
const queryString = urlObj.search;
const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString);
@@ -355,11 +361,12 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f
if (params[key] === null || params[key] === undefined) {
searchParams.delete(key);
} else if (Array.isArray(params[key])) {
+ const keyName = railsArraySyntax ? `${key}[]` : key;
params[key].forEach((val, idx) => {
if (idx === 0) {
- searchParams.set(key, val);
+ searchParams.set(keyName, val);
} else {
- searchParams.append(key, val);
+ searchParams.append(keyName, val);
}
});
} else {