Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/lib/utils')
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/error_utils.js149
-rw-r--r--app/assets/javascripts/lib/utils/file_utility.js12
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js30
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js32
-rw-r--r--app/assets/javascripts/lib/utils/vue3compat/vue_router.js11
-rw-r--r--app/assets/javascripts/lib/utils/vuex_module_mappers.js1
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 =