summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib/utils
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/lib/utils')
-rw-r--r--app/assets/javascripts/lib/utils/axios_startup_calls.js73
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js38
-rw-r--r--app/assets/javascripts/lib/utils/forms.js37
-rw-r--r--app/assets/javascripts/lib/utils/image_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/jquery_at_who.js3
-rw-r--r--app/assets/javascripts/lib/utils/poll.js6
-rw-r--r--app/assets/javascripts/lib/utils/set.js1
-rw-r--r--app/assets/javascripts/lib/utils/simple_poll.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js77
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js75
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js1
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js27
-rw-r--r--app/assets/javascripts/lib/utils/webpack.js1
13 files changed, 284 insertions, 61 deletions
diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js
index a047cebc8ab..7e2665b910c 100644
--- a/app/assets/javascripts/lib/utils/axios_startup_calls.js
+++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js
@@ -10,6 +10,32 @@ const getFullUrl = req => {
return mergeUrlParams(req.params || {}, url);
};
+const handleStartupCall = async ({ fetchCall }, req) => {
+ const res = await fetchCall;
+ if (!res.ok) {
+ throw new Error(res.statusText);
+ }
+
+ const fetchHeaders = {};
+ res.headers.forEach((val, key) => {
+ fetchHeaders[key] = val;
+ });
+
+ const data = await res.clone().json();
+
+ Object.assign(req, {
+ adapter: () =>
+ Promise.resolve({
+ data,
+ status: res.status,
+ statusText: res.statusText,
+ headers: fetchHeaders,
+ config: req,
+ request: req,
+ }),
+ });
+};
+
const setupAxiosStartupCalls = axios => {
const { startup_calls: startupCalls } = window.gl || {};
@@ -17,35 +43,28 @@ const setupAxiosStartupCalls = axios => {
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 remainingCalls = new Map(Object.entries(startupCalls));
+
+ const interceptor = axios.interceptors.request.use(async 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
- .clone()
- .json()
- .then(data => ({
- data,
- status: res.status,
- statusText: res.statusText,
- headers: fetchHeaders,
- config: req,
- request: req,
- }));
- });
+ const startupCall = remainingCalls.get(fullUrl);
+
+ if (!startupCall?.fetchCall) {
+ return req;
+ }
+
+ try {
+ await handleStartupCall(startupCall, req);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.warn(`[gitlab] Something went wrong with the startup call for "${fullUrl}"`, e);
+ }
+
+ remainingCalls.delete(fullUrl);
+
+ if (remainingCalls.size === 0) {
+ axios.interceptors.request.eject(interceptor);
}
return req;
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index e26b63fbb85..b193a8b2c9a 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -216,8 +216,9 @@ export const timeFor = (time, expiredLabel) => {
return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim();
};
+export const millisecondsPerDay = 1000 * 60 * 60 * 24;
+
export const getDayDifference = (a, b) => {
- const millisecondsPerDay = 1000 * 60 * 60 * 24;
const date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
const date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
@@ -642,6 +643,16 @@ export const secondsToMilliseconds = seconds => seconds * 1000;
export const secondsToDays = seconds => Math.round(seconds / 86400);
/**
+ * Returns the date n days after the date provided
+ *
+ * @param {Date} date the initial date
+ * @param {Number} numberOfDays number of days after
+ * @return {Date} the date following the date provided
+ */
+export const nDaysAfter = (date, numberOfDays) =>
+ new Date(newDate(date)).setDate(date.getDate() + numberOfDays);
+
+/**
* Returns the date after the date provided
*
* @param {Date} date the initial date
@@ -702,20 +713,14 @@ export const approximateDuration = (seconds = 0) => {
* @return {Date} the date object from the params
*/
export const dateFromParams = (year, month, day) => {
- const date = new Date();
-
- date.setFullYear(year);
- date.setMonth(month);
- date.setDate(day);
-
- return date;
+ return new Date(year, month, day);
};
/**
* A utility function which computes the difference in seconds
* between 2 dates.
*
- * @param {Date} startDate the start sate
+ * @param {Date} startDate the start date
* @param {Date} endDate the end date
*
* @return {Int} the difference in seconds
@@ -723,3 +728,18 @@ export const dateFromParams = (year, month, day) => {
export const differenceInSeconds = (startDate, endDate) => {
return (endDate.getTime() - startDate.getTime()) / 1000;
};
+
+/**
+ * A utility function which computes the difference in milliseconds
+ * between 2 dates.
+ *
+ * @param {Date|Int} startDate the start date. Can be either a date object or a unix timestamp.
+ * @param {Date|Int} endDate the end date. Can be either a date object or a unix timestamp. Defaults to now.
+ *
+ * @return {Int} the difference in milliseconds
+ */
+export const differenceInMilliseconds = (startDate, endDate = Date.now()) => {
+ const startDateInMS = startDate instanceof Date ? startDate.getTime() : startDate;
+ const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate;
+ return endDateInMS - startDateInMS;
+};
diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js
index ced44ab9817..1c5f6cefeda 100644
--- a/app/assets/javascripts/lib/utils/forms.js
+++ b/app/assets/javascripts/lib/utils/forms.js
@@ -14,3 +14,40 @@ export const serializeForm = form => {
return serializeFormEntries(entries);
};
+
+/**
+ * Check if the value provided is empty or not
+ *
+ * It is being used to check if a form input
+ * value has been set or not
+ *
+ * @param {String, Number, Array} - Any form value
+ * @returns {Boolean} - returns false if a value is set
+ *
+ * @example
+ * returns true for '', [], null, undefined
+ */
+export const isEmptyValue = value => value == null || value.length === 0;
+
+/**
+ * A form object serializer
+ *
+ * @param {Object} - Form Object
+ * @returns {Object} - Serialized Form Object
+ *
+ * @example
+ * Input
+ * {"project": {"value": "hello", "state": false}, "username": {"value": "john"}}
+ *
+ * Returns
+ * {"project": "hello", "username": "john"}
+ */
+export const serializeFormObject = form =>
+ Object.fromEntries(
+ Object.entries(form).reduce((acc, [name, { value }]) => {
+ if (!isEmptyValue(value)) {
+ acc.push([name, value]);
+ }
+ return acc;
+ }, []),
+ );
diff --git a/app/assets/javascripts/lib/utils/image_utility.js b/app/assets/javascripts/lib/utils/image_utility.js
index 2977ec821cb..af531f9f830 100644
--- a/app/assets/javascripts/lib/utils/image_utility.js
+++ b/app/assets/javascripts/lib/utils/image_utility.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
export function isImageLoaded(element) {
return element.complete && element.naturalHeight !== 0;
}
diff --git a/app/assets/javascripts/lib/utils/jquery_at_who.js b/app/assets/javascripts/lib/utils/jquery_at_who.js
new file mode 100644
index 00000000000..88111cb4ae4
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/jquery_at_who.js
@@ -0,0 +1,3 @@
+import 'jquery';
+import 'jquery.caret'; // required by at.js
+import '@gitlab/at.js';
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 7f0c65868c2..e8583fa951b 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -88,7 +88,11 @@ export default class Poll {
}
makeDelayedRequest(delay = 0) {
- this.timeoutID = setTimeout(() => this.makeRequest(), delay);
+ // So we don't make our specs artificially slower
+ this.timeoutID = setTimeout(
+ () => this.makeRequest(),
+ process.env.NODE_ENV !== 'test' ? delay : 1,
+ );
}
makeRequest() {
diff --git a/app/assets/javascripts/lib/utils/set.js b/app/assets/javascripts/lib/utils/set.js
index 3845d648b61..541934c4221 100644
--- a/app/assets/javascripts/lib/utils/set.js
+++ b/app/assets/javascripts/lib/utils/set.js
@@ -4,6 +4,5 @@
* @param {Set} superset The set to be considered as the superset.
* @returns {boolean}
*/
-// eslint-disable-next-line import/prefer-default-export
export const isSubset = (subset, superset) =>
Array.from(subset).every(value => superset.has(value));
diff --git a/app/assets/javascripts/lib/utils/simple_poll.js b/app/assets/javascripts/lib/utils/simple_poll.js
index 576a9ec880c..e4e9fb2e6fa 100644
--- a/app/assets/javascripts/lib/utils/simple_poll.js
+++ b/app/assets/javascripts/lib/utils/simple_poll.js
@@ -1,10 +1,12 @@
+import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
+
export default (fn, { interval = 2000, timeout = 60000 } = {}) => {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => {
- if (timeout === 0 || Date.now() - startTime < timeout) {
+ if (timeout === 0 || differenceInMilliseconds(startTime) < timeout) {
setTimeout(fn.bind(null, next, stop), interval);
} else {
reject(new Error('SIMPLE_POLL_TIMEOUT'));
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 8d23d177410..f4c6e4e3584 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, no-param-reassign, operator-assignment, consistent-return */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
+import Shortcuts from '~/behaviors/shortcuts/shortcuts';
const LINK_TAG_PATTERN = '[{text}](url)';
@@ -303,23 +304,67 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo
});
}
+/* eslint-disable @gitlab/require-i18n-strings */
+export function keypressNoteText(e) {
+ if (this.selectionStart === this.selectionEnd) {
+ return;
+ }
+ const keys = {
+ '*': '**{text}**', // wraps with bold character
+ _: '_{text}_', // wraps with italic character
+ '`': '`{text}`', // wraps with inline character
+ "'": "'{text}'", // single quotes
+ '"': '"{text}"', // double quotes
+ '[': '[{text}]', // brackets
+ '{': '{{text}}', // braces
+ '(': '({text})', // parentheses
+ '<': '<{text}>', // angle brackets
+ };
+ const tag = keys[e.key];
+
+ if (tag) {
+ e.preventDefault();
+
+ updateText({
+ tag,
+ textArea: this,
+ blockTag: '',
+ wrap: true,
+ select: '',
+ tagContent: '',
+ });
+ }
+}
+/* eslint-enable @gitlab/require-i18n-strings */
+
+export function updateTextForToolbarBtn($toolbarBtn) {
+ return updateText({
+ textArea: $toolbarBtn.closest('.md-area').find('textarea'),
+ tag: $toolbarBtn.data('mdTag'),
+ cursorOffset: $toolbarBtn.data('mdCursorOffset'),
+ blockTag: $toolbarBtn.data('mdBlock'),
+ wrap: !$toolbarBtn.data('mdPrepend'),
+ select: $toolbarBtn.data('mdSelect'),
+ tagContent: $toolbarBtn.data('mdTagContent'),
+ });
+}
+
export function addMarkdownListeners(form) {
- return $('.js-md', form)
+ $('.markdown-area', form)
+ .on('keydown', keypressNoteText)
+ .each(function attachTextareaShortcutHandlers() {
+ Shortcuts.initMarkdownEditorShortcuts($(this), updateTextForToolbarBtn);
+ });
+
+ const $allToolbarBtns = $('.js-md', form)
.off('click')
.on('click', function() {
- const $this = $(this);
- const tag = this.dataset.mdTag;
-
- return updateText({
- textArea: $this.closest('.md-area').find('textarea'),
- tag,
- cursorOffset: $this.data('mdCursorOffset'),
- blockTag: $this.data('mdBlock'),
- wrap: !$this.data('mdPrepend'),
- select: $this.data('mdSelect'),
- tagContent: $this.data('mdTagContent'),
- });
+ const $toolbarBtn = $(this);
+
+ return updateTextForToolbarBtn($toolbarBtn);
});
+
+ return $allToolbarBtns;
}
export function addEditorMarkdownListeners(editor) {
@@ -342,5 +387,11 @@ export function addEditorMarkdownListeners(editor) {
}
export function removeMarkdownListeners(form) {
+ $('.markdown-area', form)
+ .off('keydown', keypressNoteText)
+ .each(function removeTextareaShortcutHandlers() {
+ Shortcuts.removeMarkdownEditorShortcuts($(this));
+ });
+
return $('.js-md', form).off('click');
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index e2953ce330c..8ac6a44cba9 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -275,6 +275,81 @@ export const convertToSentenceCase = string => {
*/
export const convertToTitleCase = string => string.replace(/\b[a-z]/g, s => s.toUpperCase());
+const unicodeConversion = [
+ [/[ÀÁÂÃÅĀĂĄ]/g, 'A'],
+ [/[Æ]/g, 'AE'],
+ [/[ÇĆĈĊČ]/g, 'C'],
+ [/[ÈÉÊËĒĔĖĘĚ]/g, 'E'],
+ [/[ÌÍÎÏĨĪĬĮİ]/g, 'I'],
+ [/[Ððĥħ]/g, 'h'],
+ [/[ÑŃŅŇʼn]/g, 'N'],
+ [/[ÒÓÔÕØŌŎŐ]/g, 'O'],
+ [/[ÙÚÛŨŪŬŮŰŲ]/g, 'U'],
+ [/[ÝŶŸ]/g, 'Y'],
+ [/[Þñþńņň]/g, 'n'],
+ [/[ߌŜŞŠ]/g, 'S'],
+ [/[àáâãåāăąĸ]/g, 'a'],
+ [/[æ]/g, 'ae'],
+ [/[çćĉċč]/g, 'c'],
+ [/[èéêëēĕėęě]/g, 'e'],
+ [/[ìíîïĩīĭį]/g, 'i'],
+ [/[òóôõøōŏő]/g, 'o'],
+ [/[ùúûũūŭůűų]/g, 'u'],
+ [/[ýÿŷ]/g, 'y'],
+ [/[ĎĐ]/g, 'D'],
+ [/[ďđ]/g, 'd'],
+ [/[ĜĞĠĢ]/g, 'G'],
+ [/[ĝğġģŊŋſ]/g, 'g'],
+ [/[ĤĦ]/g, 'H'],
+ [/[ıśŝşš]/g, 's'],
+ [/[IJ]/g, 'IJ'],
+ [/[ij]/g, 'ij'],
+ [/[Ĵ]/g, 'J'],
+ [/[ĵ]/g, 'j'],
+ [/[Ķ]/g, 'K'],
+ [/[ķ]/g, 'k'],
+ [/[ĹĻĽĿŁ]/g, 'L'],
+ [/[ĺļľŀł]/g, 'l'],
+ [/[Œ]/g, 'OE'],
+ [/[œ]/g, 'oe'],
+ [/[ŔŖŘ]/g, 'R'],
+ [/[ŕŗř]/g, 'r'],
+ [/[ŢŤŦ]/g, 'T'],
+ [/[ţťŧ]/g, 't'],
+ [/[Ŵ]/g, 'W'],
+ [/[ŵ]/g, 'w'],
+ [/[ŹŻŽ]/g, 'Z'],
+ [/[źżž]/g, 'z'],
+ [/ö/g, 'oe'],
+ [/ü/g, 'ue'],
+ [/ä/g, 'ae'],
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [/Ö/g, 'Oe'],
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [/Ü/g, 'Ue'],
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ [/Ä/g, 'Ae'],
+];
+
+/**
+ * Converts each non-ascii character in a string to
+ * it's ascii equivalent (if defined).
+ *
+ * e.g. "Dĭd söméònê äšk fœŕ Ůnĭċődę?" => "Did someone aesk foer Unicode?"
+ *
+ * @param {String} string
+ * @returns {String}
+ */
+export const convertUnicodeToAscii = string => {
+ let convertedString = string;
+
+ unicodeConversion.forEach(([regex, replacer]) => {
+ convertedString = convertedString.replace(regex, replacer);
+ });
+
+ return convertedString;
+};
+
/**
* Splits camelCase or PascalCase words
* e.g. HelloWorld => Hello World
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index be86f336bcd..664c0dbbc84 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -1,2 +1 @@
-// eslint-disable-next-line import/prefer-default-export
export const isObject = obj => obj && obj.constructor === Object;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 8077570158a..e9c3fe0a406 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -75,7 +75,7 @@ export function getParameterValues(sParam, url = window.location) {
* @param {Boolean} options.spreadArrays - split array values into separate key/value-pairs
*/
export function mergeUrlParams(params, url, options = {}) {
- const { spreadArrays = false } = options;
+ const { spreadArrays = false, sort = false } = options;
const re = /^([^?#]*)(\?[^#]*)?(.*)/;
let merged = {};
const [, fullpath, query, fragment] = url.match(re);
@@ -108,7 +108,9 @@ export function mergeUrlParams(params, url, options = {}) {
Object.assign(merged, params);
- const newQuery = Object.keys(merged)
+ const mergedKeys = sort ? Object.keys(merged).sort() : Object.keys(merged);
+
+ const newQuery = mergedKeys
.filter(key => merged[key] !== null)
.map(key => {
let value = merged[key];
@@ -334,17 +336,32 @@ export function getWebSocketUrl(path) {
* Convert search query into an object
*
* @param {String} query from "document.location.search"
+ * @param {Object} options
+ * @param {Boolean} options.gatherArrays - gather array values into an Array
* @returns {Object}
*
* ex: "?one=1&two=2" into {one: 1, two: 2}
*/
-export function queryToObject(query) {
+export function queryToObject(query, options = {}) {
+ const { gatherArrays = false } = options;
const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query;
return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => {
const [key, value] = curr.split('=');
- if (value !== undefined) {
- accumulator[decodeURIComponent(key)] = decodeURIComponent(value);
+ if (value === undefined) {
+ return accumulator;
}
+ const decodedValue = decodeURIComponent(value);
+
+ if (gatherArrays && key.endsWith('[]')) {
+ const decodedKey = decodeURIComponent(key.slice(0, -2));
+ if (!Array.isArray(accumulator[decodedKey])) {
+ accumulator[decodedKey] = [];
+ }
+ accumulator[decodedKey].push(decodedValue);
+ } else {
+ accumulator[decodeURIComponent(key)] = decodedValue;
+ }
+
return accumulator;
}, {});
}
diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js
index 390294afcb7..622c40e0f35 100644
--- a/app/assets/javascripts/lib/utils/webpack.js
+++ b/app/assets/javascripts/lib/utils/webpack.js
@@ -1,7 +1,6 @@
import { joinPaths } from '~/lib/utils/url_utility';
// tell webpack to load assets from origin so that web workers don't break
-// eslint-disable-next-line import/prefer-default-export
export function resetServiceWorkersPublicPath() {
// __webpack_public_path__ is a global variable that can be used to adjust
// the webpack publicPath setting at runtime.