diff options
Diffstat (limited to 'app/assets/javascripts/lib')
11 files changed, 202 insertions, 52 deletions
diff --git a/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js b/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js index ad92bd4de42..9b7901685b6 100644 --- a/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js +++ b/app/assets/javascripts/lib/apollo/suppress_network_errors_during_navigation_link.js @@ -9,10 +9,6 @@ import { isNavigatingAway } from '~/lib/utils/is_navigating_away'; * @returns {ApolloLink|null} */ export const getSuppressNetworkErrorsDuringNavigationLink = () => { - if (!gon.features?.suppressApolloErrorsDuringNavigation) { - return null; - } - return onError(({ networkError }) => { if (networkError && isNavigatingAway()) { // Return an observable that will never notify any subscribers with any diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index 39bf804b54e..df2e85afe24 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -48,7 +48,6 @@ export const stripWhitespaceFromQuery = (url, path) => { export default (resolvers = {}, config = {}) => { const { - assumeImmutableResults, baseUrl, batchMax = 10, cacheConfig, @@ -161,10 +160,10 @@ export default (resolvers = {}, config = {}) => { link: appLink, cache: new InMemoryCache({ ...cacheConfig, - freezeResults: assumeImmutableResults, + freezeResults: true, }), resolvers, - assumeImmutableResults, + assumeImmutableResults: true, defaultOptions: { query: { fetchPolicy, diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 813fd3dbb1e..a82dad7e2c9 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -220,16 +220,16 @@ export const scrollToElement = (element, options = {}) => { // In the previous implementation, jQuery naturally deferred this scrolling. // Unfortunately, we're quite coupled to this implementation detail now. defer(() => { - const { duration = 200, offset = 0 } = options; + const { duration = 200, offset = 0, behavior = duration ? 'smooth' : 'auto' } = options; const y = el.getBoundingClientRect().top + window.pageYOffset + offset - contentTop(); - window.scrollTo({ top: y, behavior: duration ? 'smooth' : 'auto' }); + window.scrollTo({ top: y, behavior }); }); } }; -export const scrollToElementWithContext = (element) => { +export const scrollToElementWithContext = (element, options) => { const offsetMultiplier = -0.1; - return scrollToElement(element, { offset: window.innerHeight * offsetMultiplier }); + return scrollToElement(element, { ...options, offset: window.innerHeight * offsetMultiplier }); }; /** @@ -688,17 +688,20 @@ export const searchBy = (query = '', searchSpace = {}) => { */ export const isScopedLabel = ({ title = '' } = {}) => title.includes(SCOPED_LABEL_DELIMITER); +const scopedLabelRegex = new RegExp(`(.*)${SCOPED_LABEL_DELIMITER}.*`); + /** - * Returns the base value of the scoped label - * - * Expected Label to be an Object with `title` as a key: - * { title: 'LabelTitle', ...otherProperties }; + * Returns the key of a scoped label. + * For example: + * - returns `scoped` if the label is `scoped::value`. + * - returns `scoped::label` if the label is `scoped::label::value`. * - * @param {Object} label - * @returns String + * @param {Object} label object containing `title` property + * @returns String scoped label key, or full label if it is not a scoped label */ -export const scopedLabelKey = ({ title = '' }) => - isScopedLabel({ title }) && title.split(SCOPED_LABEL_DELIMITER)[0]; +export const scopedLabelKey = ({ title = '' }) => { + return title.replace(scopedLabelRegex, '$1'); +}; // Methods to set and get Cookie export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 }); diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue new file mode 100644 index 00000000000..733d0f69f5d --- /dev/null +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue @@ -0,0 +1,47 @@ +<script> +import { GlModal } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + cancelAction: { text: __('Cancel') }, + components: { + GlModal, + }, + props: { + primaryText: { + type: String, + required: false, + default: __('OK'), + }, + primaryVariant: { + type: String, + required: false, + default: 'confirm', + }, + }, + computed: { + primaryAction() { + return { text: this.primaryText, attributes: { variant: this.primaryVariant } }; + }, + }, + mounted() { + this.$refs.modal.show(); + }, +}; +</script> + +<template> + <gl-modal + ref="modal" + size="sm" + modal-id="confirmationModal" + body-class="gl-display-flex" + :action-primary="primaryAction" + :action-cancel="$options.cancelAction" + hide-header + @primary="$emit('confirmed')" + @hidden="$emit('closed')" + > + <div class="gl-align-self-center"><slot></slot></div> + </gl-modal> +</template> diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js new file mode 100644 index 00000000000..fdd0e045d07 --- /dev/null +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js @@ -0,0 +1,55 @@ +import Vue from 'vue'; + +export function confirmAction(message, { primaryBtnVariant, primaryBtnText } = {}) { + return new Promise((resolve) => { + let confirmed = false; + + const component = new Vue({ + components: { + ConfirmModal: () => import('./confirm_modal.vue'), + }, + render(h) { + return h( + 'confirm-modal', + { + props: { + primaryVariant: primaryBtnVariant, + primaryText: primaryBtnText, + }, + on: { + confirmed() { + confirmed = true; + }, + closed() { + component.$destroy(); + resolve(confirmed); + }, + }, + }, + [message], + ); + }, + }).$mount(); + }); +} + +export function confirmViaGlModal(message, element) { + const primaryBtnConfig = {}; + + const confirmBtnVariant = element.getAttribute('data-confirm-btn-variant'); + + if (confirmBtnVariant) { + primaryBtnConfig.primaryBtnVariant = confirmBtnVariant; + } + + const screenReaderText = + element.querySelector('.gl-sr-only')?.textContent || + element.querySelector('.sr-only')?.textContent || + element.getAttribute('aria-label'); + + if (screenReaderText) { + primaryBtnConfig.primaryBtnText = screenReaderText; + } + + return confirmAction(message, primaryBtnConfig); +} diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js index 0e5a23a5cbb..36c6545164e 100644 --- a/app/assets/javascripts/lib/utils/constants.js +++ b/app/assets/javascripts/lib/utils/constants.js @@ -24,3 +24,5 @@ export const DEFAULT_TH_CLASSES = // We set the drawer's z-index to 252 to clear flash messages that might // be displayed in the page and that have a z-index of 251. export const DRAWER_Z_INDEX = 252; + +export const MIN_USERNAME_LENGTH = 2; 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 3c446c21865..7bff2bf3e47 100644 --- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js @@ -14,33 +14,33 @@ import { s__, n__, __, sprintf } from '../../../locale'; export const getMonthNames = (abbreviated) => { if (abbreviated) { return [ - s__('Jan'), - s__('Feb'), - s__('Mar'), - s__('Apr'), - s__('May'), - s__('Jun'), - s__('Jul'), - s__('Aug'), - s__('Sep'), - s__('Oct'), - s__('Nov'), - s__('Dec'), + __('Jan'), + __('Feb'), + __('Mar'), + __('Apr'), + __('May'), + __('Jun'), + __('Jul'), + __('Aug'), + __('Sep'), + __('Oct'), + __('Nov'), + __('Dec'), ]; } return [ - s__('January'), - s__('February'), - s__('March'), - s__('April'), - s__('May'), - s__('June'), - s__('July'), - s__('August'), - s__('September'), - s__('October'), - s__('November'), - s__('December'), + __('January'), + __('February'), + __('March'), + __('April'), + __('May'), + __('June'), + __('July'), + __('August'), + __('September'), + __('October'), + __('November'), + __('December'), ]; }; diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js index b8b63bf58d4..f99a4927338 100644 --- a/app/assets/javascripts/lib/utils/file_upload.js +++ b/app/assets/javascripts/lib/utils/file_upload.js @@ -15,13 +15,17 @@ export default (buttonSelector, fileSelector) => { }); }; -export const getFilename = ({ clipboardData }) => { - let value; - if (window.clipboardData && window.clipboardData.getData) { - value = window.clipboardData.getData('Text'); - } else if (clipboardData && clipboardData.getData) { - value = clipboardData.getData('text/plain'); +export const getFilename = (file) => { + let fileName; + if (file) { + fileName = file.name; } - value = value.split('\r'); - return value[0]; + + return fileName; +}; + +export const validateImageName = (file) => { + const fileName = file.name ? file.name : 'image.png'; + const legalImageRegex = /^[\w.\-+]+\.(png|jpg|jpeg|gif|bmp|tiff|ico|webp)$/; + return legalImageRegex.test(fileName) ? fileName : 'image.png'; }; diff --git a/app/assets/javascripts/lib/utils/rails_ujs.js b/app/assets/javascripts/lib/utils/rails_ujs.js index 8b40cc7bd11..6b1985a23ba 100644 --- a/app/assets/javascripts/lib/utils/rails_ujs.js +++ b/app/assets/javascripts/lib/utils/rails_ujs.js @@ -1,4 +1,42 @@ import Rails from '@rails/ujs'; +import { confirmViaGlModal } from './confirm_via_gl_modal/confirm_via_gl_modal'; + +function monkeyPatchConfirmModal() { + /** + * This function is used to replace the `Rails.confirm` which uses `window.confirm` + * + * This function opens a confirmation modal which will resolve in a promise. + * Because the `Rails.confirm` API is synchronous, we go with a little hack here: + * + * 1. User clicks on something with `data-confirm` + * 2. We open the modal and return `false`, ending the "Rails" event chain + * 3. If the modal is closed and the user "confirmed" the action + * 1. replace the `Rails.confirm` with a function that always returns `true` + * 2. click the same element programmatically + * + * @param message {String} Message to be shown in the modal + * @param element {HTMLElement} Element that was clicked on + * @returns {boolean} + */ + function confirmViaModal(message, element) { + confirmViaGlModal(message, element) + .then((confirmed) => { + if (confirmed) { + Rails.confirm = () => true; + element.click(); + Rails.confirm = confirmViaModal; + } + }) + .catch(() => {}); + return false; + } + + Rails.confirm = confirmViaModal; +} + +if (gon?.features?.bootstrapConfirmationModals) { + monkeyPatchConfirmModal(); +} export const initRails = () => { // eslint-disable-next-line no-underscore-dangle diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 0804d792631..40dd29bea76 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -233,7 +233,7 @@ export function insertMarkdownText({ } } else if (tag.indexOf(textPlaceholder) > -1) { textToInsert = tag.replace(textPlaceholder, () => - selected.replace(/\\n/g, '\n').replace('%br', '\\n'), + selected.replace(/\\n/g, '\n').replace(/%br/g, '\\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 c70d23d06ec..e53a39cde06 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -5,6 +5,12 @@ const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`); const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`); const SHA_REGEX = /[\da-f]{40}/gi; +// About GitLab default host (overwrite in jh) +export const PROMO_HOST = 'about.gitlab.com'; + +// About Gitlab default url (overwrite in jh) +export const PROMO_URL = `https://${PROMO_HOST}`; + // Reset the cursor in a Regex so that multiple uses before a recompile don't fail function resetRegExp(regex) { regex.lastIndex = 0; /* eslint-disable-line no-param-reassign */ |