summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/lib
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/lib')
-rw-r--r--app/assets/javascripts/lib/apollo/instrumentation_link.js29
-rw-r--r--app/assets/javascripts/lib/dompurify.js6
-rw-r--r--app/assets/javascripts/lib/graphql.js20
-rw-r--r--app/assets/javascripts/lib/logger/hello.js16
-rw-r--r--app/assets/javascripts/lib/logger/hello_deferred.js5
-rw-r--r--app/assets/javascripts/lib/logger/index.js6
-rw-r--r--app/assets/javascripts/lib/utils/accessor.js28
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js11
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_format_utility.js105
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js12
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js9
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js4
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js37
13 files changed, 218 insertions, 70 deletions
diff --git a/app/assets/javascripts/lib/apollo/instrumentation_link.js b/app/assets/javascripts/lib/apollo/instrumentation_link.js
new file mode 100644
index 00000000000..2ab364557b8
--- /dev/null
+++ b/app/assets/javascripts/lib/apollo/instrumentation_link.js
@@ -0,0 +1,29 @@
+import { ApolloLink } from 'apollo-link';
+import { memoize } from 'lodash';
+
+export const FEATURE_CATEGORY_HEADER = 'x-gitlab-feature-category';
+
+/**
+ * Returns the ApolloLink (or null) used to add instrumentation metadata to the GraphQL request.
+ *
+ * - The result will be null if the `feature_category` cannot be found.
+ * - The result is memoized since the `feature_category` is the same for the entire page.
+ */
+export const getInstrumentationLink = memoize(() => {
+ const { feature_category: featureCategory } = gon;
+
+ if (!featureCategory) {
+ return null;
+ }
+
+ return new ApolloLink((operation, forward) => {
+ operation.setContext(({ headers = {} }) => ({
+ headers: {
+ ...headers,
+ [FEATURE_CATEGORY_HEADER]: featureCategory,
+ },
+ }));
+
+ return forward(operation);
+ });
+});
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index a026f76e51b..d421d66981e 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -3,7 +3,7 @@ import { getBaseURL, relativePathToAbsolute } from '~/lib/utils/url_utility';
const defaultConfig = {
// Safely allow SVG <use> tags
- ADD_TAGS: ['use'],
+ ADD_TAGS: ['use', 'gl-emoji'],
// Prevent possible XSS attacks with data-* attributes used by @rails/ujs
// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1421
FORBID_ATTR: ['data-remote', 'data-url', 'data-type', 'data-method'],
@@ -16,7 +16,7 @@ const getAllowedIconUrls = (gon = window.gon) =>
const isUrlAllowed = (url) => getAllowedIconUrls().some((allowedUrl) => url.startsWith(allowedUrl));
const isHrefSafe = (url) =>
- isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL()));
+ isUrlAllowed(url) || isUrlAllowed(relativePathToAbsolute(url, getBaseURL())) || url.match(/^#/);
const removeUnsafeHref = (node, attr) => {
if (!node.hasAttribute(attr)) {
@@ -52,4 +52,4 @@ addHook('afterSanitizeAttributes', (node) => {
}
});
-export const sanitize = (val, config = defaultConfig) => dompurifySanitize(val, config);
+export const sanitize = (val, config) => dompurifySanitize(val, { ...defaultConfig, ...config });
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 0804213cafa..b96a55fe116 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -10,6 +10,7 @@ import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf';
import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
+import { getInstrumentationLink } from './apollo/instrumentation_link';
export const fetchPolicies = {
CACHE_FIRST: 'cache-first',
@@ -140,14 +141,17 @@ export default (resolvers = {}, config = {}) => {
const appLink = ApolloLink.split(
hasSubscriptionOperation,
new ActionCableLink(),
- ApolloLink.from([
- requestCounterLink,
- performanceBarLink,
- new StartupJSLink(),
- apolloCaptchaLink,
- uploadsLink,
- requestLink,
- ]),
+ ApolloLink.from(
+ [
+ getInstrumentationLink(),
+ requestCounterLink,
+ performanceBarLink,
+ new StartupJSLink(),
+ apolloCaptchaLink,
+ uploadsLink,
+ requestLink,
+ ].filter(Boolean),
+ ),
);
return new ApolloClient({
diff --git a/app/assets/javascripts/lib/logger/hello.js b/app/assets/javascripts/lib/logger/hello.js
new file mode 100644
index 00000000000..18fa35ab55b
--- /dev/null
+++ b/app/assets/javascripts/lib/logger/hello.js
@@ -0,0 +1,16 @@
+const HANDSHAKE = String.fromCodePoint(0x1f91d);
+const MAG = String.fromCodePoint(0x1f50e);
+
+export const logHello = () => {
+ // eslint-disable-next-line no-console
+ console.log(
+ `%cWelcome to GitLab!%c
+
+Does this page need fixes or improvements? Open an issue or contribute a merge request to help make GitLab more lovable. At GitLab, everyone can contribute!
+
+${HANDSHAKE} Contribute to GitLab: https://about.gitlab.com/community/contribute/
+${MAG} Create a new GitLab issue: https://gitlab.com/gitlab-org/gitlab/-/issues/new`,
+ `padding-top: 0.5em; font-size: 2em;`,
+ 'padding-bottom: 0.5em;',
+ );
+};
diff --git a/app/assets/javascripts/lib/logger/hello_deferred.js b/app/assets/javascripts/lib/logger/hello_deferred.js
new file mode 100644
index 00000000000..ce1dd91cb37
--- /dev/null
+++ b/app/assets/javascripts/lib/logger/hello_deferred.js
@@ -0,0 +1,5 @@
+export const logHelloDeferred = async () => {
+ const { logHello } = await import(/* webpackChunkName: 'hello' */ './hello');
+
+ logHello();
+};
diff --git a/app/assets/javascripts/lib/logger/index.js b/app/assets/javascripts/lib/logger/index.js
new file mode 100644
index 00000000000..0f5353fcbed
--- /dev/null
+++ b/app/assets/javascripts/lib/logger/index.js
@@ -0,0 +1,6 @@
+/* eslint-disable no-console */
+export const LOG_PREFIX = '[gitlab]';
+
+export const logError = (message = '', ...args) => {
+ console.error(LOG_PREFIX, `${message}\n`, ...args);
+};
diff --git a/app/assets/javascripts/lib/utils/accessor.js b/app/assets/javascripts/lib/utils/accessor.js
index 39cffedcac6..d4a6d70c62c 100644
--- a/app/assets/javascripts/lib/utils/accessor.js
+++ b/app/assets/javascripts/lib/utils/accessor.js
@@ -1,4 +1,4 @@
-function isPropertyAccessSafe(base, property) {
+function canAccessProperty(base, property) {
let safe;
try {
@@ -10,7 +10,7 @@ function isPropertyAccessSafe(base, property) {
return safe;
}
-function isFunctionCallSafe(base, functionName, ...args) {
+function canCallFunction(base, functionName, ...args) {
let safe = true;
try {
@@ -22,16 +22,28 @@ function isFunctionCallSafe(base, functionName, ...args) {
return safe;
}
-function isLocalStorageAccessSafe() {
+/**
+ * Determines if `window.localStorage` is available and
+ * can be written to and read from.
+ *
+ * Important: This is not a guarantee that
+ * `localStorage.setItem` will work in all cases.
+ *
+ * `setItem` can still throw exceptions and should be
+ * surrounded with a try/catch where used.
+ *
+ * See: https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem#exceptions
+ */
+function canUseLocalStorage() {
let safe;
- const TEST_KEY = 'isLocalStorageAccessSafe';
+ const TEST_KEY = 'canUseLocalStorage';
const TEST_VALUE = 'true';
- safe = isPropertyAccessSafe(window, 'localStorage');
+ safe = canAccessProperty(window, 'localStorage');
if (!safe) return safe;
- safe = isFunctionCallSafe(window.localStorage, 'setItem', TEST_KEY, TEST_VALUE);
+ safe = canCallFunction(window.localStorage, 'setItem', TEST_KEY, TEST_VALUE);
if (safe) window.localStorage.removeItem(TEST_KEY);
@@ -39,9 +51,7 @@ function isLocalStorageAccessSafe() {
}
const AccessorUtilities = {
- isPropertyAccessSafe,
- isFunctionCallSafe,
- isLocalStorageAccessSafe,
+ canUseLocalStorage,
};
export default AccessorUtilities;
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8f86fd55d6e..fd9629499b0 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -117,7 +117,6 @@ export const handleLocationHash = () => {
};
// Check if element scrolled into viewport from above or below
-// Courtesy http://stackoverflow.com/a/7557433/414749
export const isInViewport = (el, offset = {}) => {
const rect = el.getBoundingClientRect();
const { top, left } = offset;
@@ -560,11 +559,9 @@ export const addSelectOnFocusBehaviour = (selector = '.js-select-on-focus') => {
* Method to round of values with decimal places
* with provided precision.
*
- * Taken from https://stackoverflow.com/a/7343013/414749
- *
* Eg; roundOffFloat(3.141592, 3) = 3.142
*
- * Refer to spec/javascripts/lib/utils/common_utils_spec.js for
+ * Refer to spec/frontend/lib/utils/common_utils_spec.js for
* more supported examples.
*
* @param {Float} number
@@ -581,7 +578,7 @@ export const roundOffFloat = (number, precision = 0) => {
*
* Eg; roundToNearestHalf(3.141592) = 3, roundToNearestHalf(3.41592) = 3.5
*
- * Refer to spec/javascripts/lib/utils/common_utils_spec.js for
+ * Refer to spec/frontend/lib/utils/common_utils_spec.js for
* more supported examples.
*
* @param {Float} number
@@ -595,7 +592,7 @@ export const roundToNearestHalf = (num) => Math.round(num * 2).toFixed() / 2;
*
* Eg; roundDownFloat(3.141592, 3) = 3.141
*
- * Refer to spec/javascripts/lib/utils/common_utils_spec.js for
+ * Refer to spec/frontend/lib/utils/common_utils_spec.js for
* more supported examples.
*
* @param {Float} number
@@ -645,7 +642,7 @@ export const NavigationType = {
* matched with our query.
*
* You can learn more about behaviour of this method by referring to tests
- * within `spec/javascripts/lib/utils/common_utils_spec.js`.
+ * within `spec/frontend/lib/utils/common_utils_spec.js`.
*
* @param {string} query String to search for
* @param {object} searchSpace Object containing properties to search in for `query`
diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
index 246f290a90a..0a35efb0ac8 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -1,5 +1,5 @@
import dateFormat from 'dateformat';
-import { isString, mapValues, reduce } from 'lodash';
+import { isString, mapValues, reduce, isDate } from 'lodash';
import { s__, n__, __ } from '../../../locale';
/**
@@ -258,3 +258,106 @@ export const parseSeconds = (
return periodCount;
});
};
+
+/**
+ * Pads given items with zeros to reach a length of 2 characters.
+ *
+ * @param {...any} args Items to be padded.
+ * @returns {Array<String>} Padded items.
+ */
+export const padWithZeros = (...args) => args.map((arg) => `${arg}`.padStart(2, '0'));
+
+/**
+ * This removes the timezone from an ISO date string.
+ * This can be useful when populating date/time fields along with a distinct timezone selector, in
+ * which case we'd want to ignore the timezone's offset when populating the date and time.
+ *
+ * Examples:
+ * stripTimezoneFromISODate('2021-08-16T00:00:00.000-02:00') => '2021-08-16T00:00:00.000'
+ * stripTimezoneFromISODate('2021-08-16T00:00:00.000Z') => '2021-08-16T00:00:00.000'
+ *
+ * @param {String} date The ISO date string representation.
+ * @returns {String} The ISO date string without the timezone.
+ */
+export const stripTimezoneFromISODate = (date) => {
+ if (Number.isNaN(Date.parse(date))) {
+ return null;
+ }
+ return date.replace(/(Z|[+-]\d{2}:\d{2})$/, '');
+};
+
+/**
+ * Extracts the year, month and day from a Date instance and returns them in an object.
+ * For example:
+ * dateToYearMonthDate(new Date('2021-08-16')) => { year: '2021', month: '08', day: '16' }
+ *
+ * @param {Date} date The date to be parsed
+ * @returns {Object} An object containing the extracted year, month and day.
+ */
+export const dateToYearMonthDate = (date) => {
+ if (!isDate(date)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Argument should be a Date instance');
+ }
+ const [month, day] = padWithZeros(date.getMonth() + 1, date.getDate());
+ return {
+ year: `${date.getFullYear()}`,
+ month,
+ day,
+ };
+};
+
+/**
+ * Extracts the hours and minutes from a string representing a time.
+ * For example:
+ * timeToHoursMinutes('12:46') => { hours: '12', minutes: '46' }
+ *
+ * @param {String} time The time to be parsed in the form HH:MM.
+ * @returns {Object} An object containing the hours and minutes.
+ */
+export const timeToHoursMinutes = (time = '') => {
+ if (!time || !time.match(/\d{1,2}:\d{1,2}/)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Invalid time provided');
+ }
+ const [hours, minutes] = padWithZeros(...time.split(':'));
+ return { hours, minutes };
+};
+
+/**
+ * This combines a date and a time and returns the computed Date's ISO string representation.
+ *
+ * @param {Date} date Date object representing the base date.
+ * @param {String} time String representing the time to be used, in the form HH:MM.
+ * @param {String} offset An optional Date-compatible offset.
+ * @returns {String} The combined Date's ISO string representation.
+ */
+export const dateAndTimeToISOString = (date, time, offset = '') => {
+ const { year, month, day } = dateToYearMonthDate(date);
+ const { hours, minutes } = timeToHoursMinutes(time);
+ const dateString = `${year}-${month}-${day}T${hours}:${minutes}:00.000${offset || 'Z'}`;
+ if (Number.isNaN(Date.parse(dateString))) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Could not initialize date');
+ }
+ return dateString;
+};
+
+/**
+ * Converts a Date instance to time input-compatible value consisting in a 2-digits hours and
+ * minutes, separated by a semi-colon, in the 24-hours format.
+ *
+ * @param {Date} date Date to be converted
+ * @returns {String} time input-compatible string in the form HH:MM.
+ */
+export const dateToTimeInputValue = (date) => {
+ if (!isDate(date)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Argument should be a Date instance');
+ }
+ return date.toLocaleTimeString([], {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ });
+};
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index f11c7658a88..f7687a929de 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -77,3 +77,15 @@ export const isElementVisible = (element) =>
* @returns {Boolean} `true` if the element is currently hidden, otherwise false
*/
export const isElementHidden = (element) => !isElementVisible(element);
+
+export const getParents = (element) => {
+ const parents = [];
+ let parent = element.parentNode;
+
+ do {
+ parents.push(parent);
+ parent = parent.parentNode;
+ } while (parent);
+
+ return parents;
+};
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index f3dedb7726a..f46263c0e4d 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -69,19 +69,20 @@ export function bytesToGiB(number) {
* representation (e.g., giving it 1500 yields 1.5 KB).
*
* @param {Number} size
+ * @param {Number} digits - The number of digits to appear after the decimal point
* @returns {String}
*/
-export function numberToHumanSize(size) {
+export function numberToHumanSize(size, digits = 2) {
const abs = Math.abs(size);
if (abs < BYTES_IN_KIB) {
return sprintf(__('%{size} bytes'), { size });
} else if (abs < BYTES_IN_KIB ** 2) {
- return sprintf(__('%{size} KiB'), { size: bytesToKiB(size).toFixed(2) });
+ return sprintf(__('%{size} KiB'), { size: bytesToKiB(size).toFixed(digits) });
} else if (abs < BYTES_IN_KIB ** 3) {
- return sprintf(__('%{size} MiB'), { size: bytesToMiB(size).toFixed(2) });
+ return sprintf(__('%{size} MiB'), { size: bytesToMiB(size).toFixed(digits) });
}
- return sprintf(__('%{size} GiB'), { size: bytesToGiB(size).toFixed(2) });
+ return sprintf(__('%{size} GiB'), { size: bytesToGiB(size).toFixed(digits) });
}
/**
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js
index 6ff2af47dd8..0804d792631 100644
--- a/app/assets/javascripts/lib/utils/text_markdown.js
+++ b/app/assets/javascripts/lib/utils/text_markdown.js
@@ -232,7 +232,9 @@ export function insertMarkdownText({
.join('\n');
}
} else if (tag.indexOf(textPlaceholder) > -1) {
- textToInsert = tag.replace(textPlaceholder, () => selected.replace(/\\n/g, '\n'));
+ textToInsert = tag.replace(textPlaceholder, () =>
+ selected.replace(/\\n/g, '\n').replace('%br', '\\n'),
+ );
} else {
textToInsert = String(startChar) + tag + selected + (wrap ? tag : '');
}
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index e9772232eaf..bca0e45d98d 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -418,43 +418,6 @@ export const urlParamsToArray = (path = '') =>
export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
/**
- * Accepts encoding string which includes query params being
- * sent to URL.
- *
- * @param {string} path Query param string
- *
- * @returns {object} Query params object containing key-value pairs
- * with both key and values decoded into plain string.
- *
- * @deprecated Please use `queryToObject(query, { gatherArrays: true });` instead. See https://gitlab.com/gitlab-org/gitlab/-/issues/328845
- */
-export const urlParamsToObject = (path = '') =>
- splitPath(path).reduce((dataParam, filterParam) => {
- if (filterParam === '') {
- return dataParam;
- }
-
- const data = dataParam;
- let [key, value] = filterParam.split('=');
- key = /%\w+/g.test(key) ? decodeURIComponent(key) : key;
- const isArray = key.includes('[]');
- key = key.replace('[]', '');
- value = decodeURIComponent(value.replace(/\+/g, ' '));
-
- if (isArray) {
- if (!data[key]) {
- data[key] = [];
- }
-
- data[key].push(value);
- } else {
- data[key] = value;
- }
-
- return data;
- }, {});
-
-/**
* Convert search query into an object
*
* @param {String} query from "document.location.search"