diff options
Diffstat (limited to 'app/assets/javascripts/lib')
-rw-r--r-- | app/assets/javascripts/lib/apollo/instrumentation_link.js | 29 | ||||
-rw-r--r-- | app/assets/javascripts/lib/dompurify.js | 6 | ||||
-rw-r--r-- | app/assets/javascripts/lib/graphql.js | 20 | ||||
-rw-r--r-- | app/assets/javascripts/lib/logger/hello.js | 16 | ||||
-rw-r--r-- | app/assets/javascripts/lib/logger/hello_deferred.js | 5 | ||||
-rw-r--r-- | app/assets/javascripts/lib/logger/index.js | 6 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/accessor.js | 28 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/common_utils.js | 11 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/datetime/date_format_utility.js | 105 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/dom_utils.js | 12 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/number_utils.js | 9 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_markdown.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/url_utility.js | 37 |
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" |