diff options
Diffstat (limited to 'app/assets/javascripts/lib/utils')
-rw-r--r-- | app/assets/javascripts/lib/utils/constants.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/error_utils.js | 149 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/file_utility.js | 12 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_utility.js | 30 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/url_utility.js | 32 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/vue3compat/vue_router.js | 11 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/vuex_module_mappers.js | 1 |
7 files changed, 202 insertions, 34 deletions
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js index d1e5e4eea13..aceae188b73 100644 --- a/app/assets/javascripts/lib/utils/constants.js +++ b/app/assets/javascripts/lib/utils/constants.js @@ -15,6 +15,7 @@ export const DATETIME_RANGE_TYPES = { export const BV_SHOW_MODAL = 'bv::show::modal'; export const BV_HIDE_MODAL = 'bv::hide::modal'; export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip'; +export const BV_SHOW_TOOLTIP = 'bv::show::tooltip'; export const BV_DROPDOWN_SHOW = 'bv::dropdown::show'; export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide'; diff --git a/app/assets/javascripts/lib/utils/error_utils.js b/app/assets/javascripts/lib/utils/error_utils.js new file mode 100644 index 00000000000..82dba803c3e --- /dev/null +++ b/app/assets/javascripts/lib/utils/error_utils.js @@ -0,0 +1,149 @@ +import { isEmpty, isString, isObject } from 'lodash'; +import { sprintf, __ } from '~/locale'; + +export class ActiveModelError extends Error { + constructor(errorAttributeMap = {}, ...params) { + // Pass remaining arguments (including vendor specific ones) to parent constructor + super(...params); + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, ActiveModelError); + } + + this.name = 'ActiveModelError'; + // Custom debugging information + this.errorAttributeMap = errorAttributeMap; + } +} + +const DEFAULT_ERROR = { + message: __('Something went wrong. Please try again.'), + links: {}, +}; + +/** + * @typedef {Object<ErrorAttribute,ErrorType[]>} ErrorAttributeMap - Map of attributes to error details + * @typedef {string} ErrorAttribute - the error attribute https://api.rubyonrails.org/v7.0.4.2/classes/ActiveModel/Error.html + * @typedef {string} ErrorType - the error type https://api.rubyonrails.org/v7.0.4.2/classes/ActiveModel/Error.html + * + * @example { "email": ["taken", ...] } + * // returns `${UNLINKED_ACCOUNT_ERROR}`, i.e. the `EMAIL_TAKEN_ERROR_TYPE` error message + * + * @param {ErrorAttributeMap} errorAttributeMap + * @param {Object} errorDictionary + * @returns {(null|string)} null or error message if found + */ +function getMessageFromType(errorAttributeMap = {}, errorDictionary = {}) { + if (!isObject(errorAttributeMap)) { + return null; + } + + return Object.keys(errorAttributeMap).reduce((_, attribute) => { + const errorType = errorAttributeMap[attribute].find( + (type) => errorDictionary[`${attribute}:${type}`.toLowerCase()], + ); + if (errorType) { + return errorDictionary[`${attribute}:${errorType}`.toLowerCase()]; + } + + return null; + }, null); +} + +/** + * @example "Email has already been taken, Email is invalid" + * // returns `${UNLINKED_ACCOUNT_ERROR}`, i.e. the `EMAIL_TAKEN_ERROR_TYPE` error message + * + * @param {string} errorString + * @param {Object} errorDictionary + * @returns {(null|string)} null or error message if found + */ +function getMessageFromErrorString(errorString, errorDictionary = {}) { + if (isEmpty(errorString) || !isString(errorString)) { + return null; + } + + const messages = errorString.split(', '); + const errorMessage = messages.find((message) => errorDictionary[message.toLowerCase()]); + if (errorMessage) { + return errorDictionary[errorMessage.toLowerCase()]; + } + + return { + message: errorString, + links: {}, + }; +} + +/** + * Receives an Error and attempts to extract the `errorAttributeMap` in + * case it is an `ActiveModelError` and returns the message if it exists. + * If a match is not found it will attempt to map a message from the + * Error.message to be returned. + * Otherwise, it will return a general error message. + * + * @param {Error|String} systemError + * @param {Object} errorDictionary + * @param {Object} defaultError + * @returns error message + */ +export function mapSystemToFriendlyError( + systemError, + errorDictionary = {}, + defaultError = DEFAULT_ERROR, +) { + if (systemError instanceof String || typeof systemError === 'string') { + const messageFromErrorString = getMessageFromErrorString(systemError, errorDictionary); + if (messageFromErrorString) { + return messageFromErrorString; + } + return defaultError; + } + + if (!(systemError instanceof Error)) { + return defaultError; + } + + const { errorAttributeMap, message } = systemError; + const messageFromType = getMessageFromType(errorAttributeMap, errorDictionary); + if (messageFromType) { + return messageFromType; + } + + const messageFromErrorString = getMessageFromErrorString(message, errorDictionary); + if (messageFromErrorString) { + return messageFromErrorString; + } + + return defaultError; +} + +function generateLinks(links) { + return Object.keys(links).reduce((allLinks, link) => { + /* eslint-disable-next-line @gitlab/require-i18n-strings */ + const linkStart = `${link}Start`; + /* eslint-disable-next-line @gitlab/require-i18n-strings */ + const linkEnd = `${link}End`; + + return { + ...allLinks, + [linkStart]: `<a href="${links[link]}" target="_blank" rel="noopener noreferrer">`, + [linkEnd]: '</a>', + }; + }, {}); +} + +export const generateHelpTextWithLinks = (error) => { + if (isString(error)) { + return error; + } + + if (isEmpty(error)) { + /* eslint-disable-next-line @gitlab/require-i18n-strings */ + throw new Error('The error cannot be empty.'); + } + + const links = generateLinks(error.links); + return sprintf(error.message, links, false); +}; diff --git a/app/assets/javascripts/lib/utils/file_utility.js b/app/assets/javascripts/lib/utils/file_utility.js new file mode 100644 index 00000000000..e5a41f3b042 --- /dev/null +++ b/app/assets/javascripts/lib/utils/file_utility.js @@ -0,0 +1,12 @@ +/** + * Takes a file object and returns a data uri of its contents. + * + * @param {File} file + */ +export function readFileAsDataURL(file) { + return new Promise((resolve) => { + const reader = new FileReader(); + reader.addEventListener('load', (e) => resolve(e.target.result), { once: true }); + reader.readAsDataURL(file); + }); +} diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 42f481261a2..31e16f7b4db 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -167,36 +167,6 @@ export const truncateWidth = (string, options = {}) => { */ export const truncateSha = (sha) => sha.substring(0, 8); -const ELLIPSIS_CHAR = '…'; -export const truncatePathMiddleToLength = (text, maxWidth) => { - let returnText = text; - let ellipsisCount = 0; - - while (returnText.length >= maxWidth) { - const textSplit = returnText.split('/').filter((s) => s !== ELLIPSIS_CHAR); - - if (textSplit.length === 0) { - // There are n - 1 path separators for n segments, so 2n - 1 <= maxWidth - const maxSegments = Math.floor((maxWidth + 1) / 2); - return new Array(maxSegments).fill(ELLIPSIS_CHAR).join('/'); - } - - const middleIndex = Math.floor(textSplit.length / 2); - - returnText = textSplit - .slice(0, middleIndex) - .concat( - new Array(ellipsisCount + 1).fill().map(() => ELLIPSIS_CHAR), - textSplit.slice(middleIndex + 1), - ) - .join('/'); - - ellipsisCount += 1; - } - - return returnText; -}; - /** * Capitalizes first character * diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 85740117c00..08c98298121 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -20,6 +20,7 @@ export const PROMO_HOST = `about.${DOMAIN}`; // about.gitlab.com // About Gitlab default url export const PROMO_URL = `https://${PROMO_HOST}`; +// eslint-disable-next-line no-restricted-syntax export const DOCS_URL_IN_EE_DIR = `${DOCS_URL}/ee`; // Reset the cursor in a Regex so that multiple uses before a recompile don't fail @@ -686,11 +687,23 @@ export function redirectTo(url) { } /** - * Navigates to a URL - * @param {*} url - url to navigate to + * Navigates to a URL. + * + * If destination is a querystring, it will be automatically transformed into a fully qualified URL. + * If the URL is not a safe URL (see isSafeURL implementation), this function will log an exception into Sentry. + * + * @param {*} destination - url to navigate to. This can be a fully qualified URL or a querystring. * @param {*} external - if true, open a new page or tab */ -export function visitUrl(url, external = false) { +export function visitUrl(destination, external = false) { + let url = destination; + + if (destination.startsWith('?')) { + const currentUrl = new URL(window.location.href); + currentUrl.search = destination; + url = currentUrl.toString(); + } + if (!isSafeURL(url)) { // For now log this to Sentry and do not block the execution. // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121551#note_1408873600 @@ -713,3 +726,16 @@ export function visitUrl(url, external = false) { export function refreshCurrentPage() { visitUrl(window.location.href); } + +// Adds a ref_type param to the path if refType is available +export function buildURLwithRefType({ base = window.location.origin, path, refType = null }) { + const url = new URL('', base); + url.pathname = path; // This assignment does proper _escapes_ + + if (refType) { + url.searchParams.set('ref_type', refType.toLowerCase()); + } else { + url.searchParams.delete('ref_type'); + } + return url.pathname + url.search; +} diff --git a/app/assets/javascripts/lib/utils/vue3compat/vue_router.js b/app/assets/javascripts/lib/utils/vue3compat/vue_router.js index 62054d5a80d..daea9815d60 100644 --- a/app/assets/javascripts/lib/utils/vue3compat/vue_router.js +++ b/app/assets/javascripts/lib/utils/vue3compat/vue_router.js @@ -67,7 +67,16 @@ const transformers = { const transformOptions = (options = {}) => { const defaultConfig = { - routes: [], + routes: [ + { + path: '/', + component: { + render() { + return ''; + }, + }, + }, + ], history: createWebHashHistory(), }; return Object.keys(options).reduce((acc, key) => { diff --git a/app/assets/javascripts/lib/utils/vuex_module_mappers.js b/app/assets/javascripts/lib/utils/vuex_module_mappers.js index 95a794dd268..5298eb67c2b 100644 --- a/app/assets/javascripts/lib/utils/vuex_module_mappers.js +++ b/app/assets/javascripts/lib/utils/vuex_module_mappers.js @@ -1,4 +1,5 @@ import { mapValues, isString } from 'lodash'; +// eslint-disable-next-line no-restricted-imports import { mapState, mapActions } from 'vuex'; export const REQUIRE_STRING_ERROR_MESSAGE = |