From 7881eb30eaa8b01dbcfe87faa09927c75c7d6e45 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 20 Dec 2019 09:07:57 +0000 Subject: Add latest changes from gitlab-org/gitlab@12-6-stable-ee --- app/assets/javascripts/lib/utils/axios_utils.js | 4 +- app/assets/javascripts/lib/utils/common_utils.js | 22 ++- .../javascripts/lib/utils/datetime_utility.js | 162 +++++++++++---------- app/assets/javascripts/lib/utils/http_status.js | 1 + .../javascripts/lib/utils/logoutput_behaviours.js | 47 ------ .../suppress_ajax_errors_during_navigation.js | 4 +- app/assets/javascripts/lib/utils/text_markdown.js | 38 ++--- app/assets/javascripts/lib/utils/text_utility.js | 2 +- app/assets/javascripts/lib/utils/url_utility.js | 104 ++++++++++++- 9 files changed, 225 insertions(+), 159 deletions(-) delete mode 100644 app/assets/javascripts/lib/utils/logoutput_behaviours.js (limited to 'app/assets/javascripts/lib') diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js index a04fe609015..4eec5bffc66 100644 --- a/app/assets/javascripts/lib/utils/axios_utils.js +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -33,11 +33,9 @@ window.addEventListener('beforeunload', () => { // Ignore AJAX errors caused by requests // being cancelled due to browser navigation -const { gon } = window; -const featureFlagEnabled = gon && gon.features && gon.features.suppressAjaxNavigationErrors; axios.interceptors.response.use( response => response, - err => suppressAjaxErrorsDuringNavigation(err, isUserNavigating, featureFlagEnabled), + err => suppressAjaxErrorsDuringNavigation(err, isUserNavigating), ); export default axios; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 177ae4f9838..e4001e94478 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -5,7 +5,7 @@ import $ from 'jquery'; import axios from './axios_utils'; import { getLocationHash } from './url_utility'; -import { convertToCamelCase } from './text_utility'; +import { convertToCamelCase, convertToSnakeCase } from './text_utility'; import { isObject } from './type_utility'; import breakpointInstance from '../../breakpoints'; @@ -490,6 +490,8 @@ export const historyPushState = newUrl => { */ export const parseBoolean = value => (value && value.toString()) === 'true'; +export const BACKOFF_TIMEOUT = 'BACKOFF_TIMEOUT'; + /** * @callback backOffCallback * @param {Function} next @@ -541,7 +543,7 @@ export const backOff = (fn, timeout = 60000) => { timeElapsed += nextInterval; nextInterval = Math.min(nextInterval + nextInterval, maxInterval); } else { - reject(new Error('BACKOFF_TIMEOUT')); + reject(new Error(BACKOFF_TIMEOUT)); } }; @@ -697,6 +699,22 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => { }, initial); }; +/** + * Converts all the object keys to snake case + * + * @param {Object} obj Object to transform + * @returns {Object} + */ +// Follow up to add additional options param: +// https://gitlab.com/gitlab-org/gitlab/issues/39173 +export const convertObjectPropsToSnakeCase = (obj = {}) => + obj + ? Object.entries(obj).reduce( + (acc, [key, value]) => ({ ...acc, [convertToSnakeCase(key)]: value }), + {}, + ) + : {}; + export const imagePath = imgUrl => `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/${imgUrl}`; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 28143859e4c..996692bacb3 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import _ from 'underscore'; -import timeago from 'timeago.js'; +import * as timeago from 'timeago.js'; import dateFormat from 'dateformat'; import { languageCode, s__, __, n__ } from '../../locale'; @@ -92,90 +92,80 @@ export const formatDate = (datetime, format = 'mmm d, yyyy h:MMtt Z') => { */ const timeagoLanguageCode = languageCode().replace(/-/g, '_'); -let timeagoInstance; - /** - * Sets a timeago Instance + * Registers timeago locales */ -export const getTimeago = () => { - if (!timeagoInstance) { - const memoizedLocaleRemaining = () => { - const cache = []; - - const timeAgoLocaleRemaining = [ - () => [s__('Timeago|just now'), s__('Timeago|right now')], - () => [s__('Timeago|just now'), s__('Timeago|%s seconds remaining')], - () => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], - () => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], - () => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')], - () => [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')], - () => [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')], - () => [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], - () => [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')], - () => [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], - () => [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')], - () => [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], - () => [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], - () => [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], - ]; - - return (number, index) => { - if (cache[index]) { - return cache[index]; - } - cache[index] = timeAgoLocaleRemaining[index] && timeAgoLocaleRemaining[index](); - return cache[index]; - }; - }; - - const memoizedLocale = () => { - const cache = []; - - const timeAgoLocale = [ - () => [s__('Timeago|just now'), s__('Timeago|right now')], - () => [s__('Timeago|just now'), s__('Timeago|in %s seconds')], - () => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], - () => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], - () => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')], - () => [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')], - () => [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')], - () => [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], - () => [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')], - () => [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], - () => [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')], - () => [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], - () => [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], - () => [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], - ]; - - return (number, index) => { - if (cache[index]) { - return cache[index]; - } - cache[index] = timeAgoLocale[index] && timeAgoLocale[index](); - return cache[index]; - }; - }; - - timeago.register(timeagoLanguageCode, memoizedLocale()); - timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining()); - - timeagoInstance = timeago(); - } +const memoizedLocaleRemaining = () => { + const cache = []; + + const timeAgoLocaleRemaining = [ + () => [s__('Timeago|just now'), s__('Timeago|right now')], + () => [s__('Timeago|just now'), s__('Timeago|%s seconds remaining')], + () => [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], + () => [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], + () => [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')], + () => [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')], + () => [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')], + () => [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], + () => [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')], + () => [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], + () => [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')], + () => [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], + () => [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], + () => [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], + ]; + + return (number, index) => { + if (cache[index]) { + return cache[index]; + } + cache[index] = timeAgoLocaleRemaining[index] && timeAgoLocaleRemaining[index](); + return cache[index]; + }; +}; + +const memoizedLocale = () => { + const cache = []; + + const timeAgoLocale = [ + () => [s__('Timeago|just now'), s__('Timeago|right now')], + () => [s__('Timeago|just now'), s__('Timeago|in %s seconds')], + () => [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], + () => [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], + () => [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')], + () => [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')], + () => [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')], + () => [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], + () => [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')], + () => [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], + () => [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')], + () => [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], + () => [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], + () => [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], + ]; - return timeagoInstance; + return (number, index) => { + if (cache[index]) { + return cache[index]; + } + cache[index] = timeAgoLocale[index] && timeAgoLocale[index](); + return cache[index]; + }; }; +timeago.register(timeagoLanguageCode, memoizedLocale()); +timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining()); + +export const getTimeago = () => timeago; + /** * For the given elements, sets a tooltip with a formatted date. * @param {JQuery} $timeagoEls * @param {Boolean} setTimeago */ export const localTimeAgo = ($timeagoEls, setTimeago = true) => { - getTimeago(); - $timeagoEls.each((i, el) => { - $(el).text(timeagoInstance.format($(el).attr('datetime'), timeagoLanguageCode)); + $(el).text(timeago.format($(el).attr('datetime'), timeagoLanguageCode)); }); if (!setTimeago) { @@ -207,9 +197,7 @@ export const timeFor = (time, expiredLabel) => { if (new Date(time) < new Date()) { return expiredLabel || s__('Timeago|Past due'); } - return getTimeago() - .format(time, `${timeagoLanguageCode}-remaining`) - .trim(); + return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim(); }; export const getDayDifference = (a, b) => { @@ -459,7 +447,7 @@ export const parsePikadayDate = dateString => { /** * Used `onSelect` method in pickaday * @param {Date} date UTC format - * @return {String} Date formated in yyyy-mm-dd + * @return {String} Date formatted in yyyy-mm-dd */ export const pikadayToString = date => { const day = pad(date.getDate()); @@ -525,8 +513,8 @@ export const stringifyTime = (timeObject, fullNameFormat = false) => { if (fullNameFormat && isNonZero) { // Remove traling 's' if unit value is singular - const formatedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, ''); - return `${memo} ${unitValue} ${formatedUnitName}`; + const formattedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, ''); + return `${memo} ${unitValue} ${formattedUnitName}`; } return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; @@ -602,3 +590,19 @@ export const getDatesInRange = (d1, d2, formatter = x => x) => { * @return {Number} number of milliseconds */ export const secondsToMilliseconds = seconds => seconds * 1000; + +/** + * Converts the supplied number of seconds to days. + * + * @param {Number} seconds + * @return {Number} number of days + */ +export const secondsToDays = seconds => Math.round(seconds / 86400); + +/** + * Returns the date after the date provided + * + * @param {Date} date the initial date + * @return {Date} the date following the date provided + */ +export const dayAfter = date => new Date(newDate(date).setDate(date.getDate() + 1)); diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index 5e5d10883a3..1c7d59054dc 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -21,6 +21,7 @@ const httpStatusCodes = { NOT_FOUND: 404, GONE: 410, UNPROCESSABLE_ENTITY: 422, + SERVICE_UNAVAILABLE: 503, }; export const successCodes = [ diff --git a/app/assets/javascripts/lib/utils/logoutput_behaviours.js b/app/assets/javascripts/lib/utils/logoutput_behaviours.js deleted file mode 100644 index 41b57025cc9..00000000000 --- a/app/assets/javascripts/lib/utils/logoutput_behaviours.js +++ /dev/null @@ -1,47 +0,0 @@ -import $ from 'jquery'; -import { - canScroll, - isScrolledToBottom, - isScrolledToTop, - isScrolledToMiddle, - toggleDisableButton, -} from './scroll_utils'; - -export default class LogOutputBehaviours { - constructor() { - // Scroll buttons - this.$scrollTopBtn = $('.js-scroll-up'); - this.$scrollBottomBtn = $('.js-scroll-down'); - - this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this)); - this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this)); - } - - toggleScroll() { - if (canScroll()) { - if (isScrolledToMiddle()) { - // User is in the middle of the log - - toggleDisableButton(this.$scrollTopBtn, false); - toggleDisableButton(this.$scrollBottomBtn, false); - } else if (isScrolledToTop()) { - // User is at Top of Log - - toggleDisableButton(this.$scrollTopBtn, true); - toggleDisableButton(this.$scrollBottomBtn, false); - } else if (isScrolledToBottom()) { - // User is at the bottom of the build log. - - toggleDisableButton(this.$scrollTopBtn, false); - toggleDisableButton(this.$scrollBottomBtn, true); - } - } else { - toggleDisableButton(this.$scrollTopBtn, true); - toggleDisableButton(this.$scrollBottomBtn, true); - } - } - - toggleScrollAnimation(toggle) { - this.$scrollBottomBtn.toggleClass('animate', toggle); - } -} diff --git a/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js b/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js index 4c61da9b862..fb4d9b7de9c 100644 --- a/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js +++ b/app/assets/javascripts/lib/utils/suppress_ajax_errors_during_navigation.js @@ -2,8 +2,8 @@ * An Axios error interceptor that suppresses AJAX errors caused * by the request being cancelled when the user navigates to a new page */ -export default (err, isUserNavigating, featureFlagEnabled) => { - if (featureFlagEnabled && isUserNavigating && err.code === 'ECONNABORTED') { +export default (err, isUserNavigating) => { + if (isUserNavigating && err.code === 'ECONNABORTED') { // If the user is navigating away from the current page, // prevent .then() and .catch() handlers from being // called by returning a Promise that never resolves diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 2e0270ee42f..cccf9ad311c 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, no-var, no-param-reassign, one-var, operator-assignment, no-else-return, consistent-return */ +/* eslint-disable func-names, no-param-reassign, operator-assignment, no-else-return, consistent-return */ import $ from 'jquery'; import { insertText } from '~/lib/utils/common_utils'; @@ -13,8 +13,7 @@ function addBlockTags(blockTag, selected) { } function lineBefore(text, textarea) { - var split; - split = text + const split = text .substring(0, textarea.selectionStart) .trim() .split('\n'); @@ -80,7 +79,7 @@ function moveCursor({ editorSelectionStart, editorSelectionEnd, }) { - var pos; + let pos; if (textArea && !textArea.setSelectionRange) { return; } @@ -132,18 +131,13 @@ export function insertMarkdownText({ select, editor, }) { - var textToInsert, - selectedSplit, - startChar, - removedLastNewLine, - removedFirstNewLine, - currentLineEmpty, - lastNewLine, - editorSelectionStart, - editorSelectionEnd; - removedLastNewLine = false; - removedFirstNewLine = false; - currentLineEmpty = false; + let removedLastNewLine = false; + let removedFirstNewLine = false; + let currentLineEmpty = false; + let editorSelectionStart; + let editorSelectionEnd; + let lastNewLine; + let textToInsert; if (editor) { const selectionRange = editor.getSelectionRange(); @@ -186,7 +180,7 @@ export function insertMarkdownText({ } } - selectedSplit = selected.split('\n'); + const selectedSplit = selected.split('\n'); if (editor && !wrap) { lastNewLine = editor.getValue().split('\n')[editorSelectionStart.row]; @@ -207,8 +201,7 @@ export function insertMarkdownText({ (textArea && textArea.selectionStart === 0) || (editor && editorSelectionStart.column === 0 && editorSelectionStart.row === 0); - startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : ''; - + const startChar = !wrap && !currentLineEmpty && !isBeginning ? '\n' : ''; const textPlaceholder = '{text}'; if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { @@ -263,11 +256,10 @@ export function insertMarkdownText({ } function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagContent }) { - var $textArea, selected, text; - $textArea = $(textArea); + const $textArea = $(textArea); textArea = $textArea.get(0); - text = $textArea.val(); - selected = selectedText(text, textArea) || tagContent; + const text = $textArea.val(); + const selected = selectedText(text, textArea) || tagContent; $textArea.focus(); return insertMarkdownText({ textArea, diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 0c194d67bce..6bbf118d7d1 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -72,7 +72,7 @@ export const truncate = (string, maxLength) => `${string.substr(0, maxLength - 3 * @param {String} sha * @returns {String} */ -export const truncateSha = sha => sha.substr(0, 8); +export const truncateSha = sha => sha.substring(0, 8); const ELLIPSIS_CHAR = '…'; export const truncatePathMiddleToLength = (text, maxWidth) => { diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 4be0d05a9b7..d48678c21f6 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -1,4 +1,6 @@ -import { join as joinPaths } from 'path'; +const PATH_SEPARATOR = '/'; +const PATH_SEPARATOR_LEADING_REGEX = new RegExp(`^${PATH_SEPARATOR}+`); +const PATH_SEPARATOR_ENDING_REGEX = new RegExp(`${PATH_SEPARATOR}+$`); // Returns a decoded url parameter value // - Treats '+' as '%20' @@ -6,6 +8,37 @@ function decodeUrlParameter(val) { return decodeURIComponent(val.replace(/\+/g, '%20')); } +function cleanLeadingSeparator(path) { + return path.replace(PATH_SEPARATOR_LEADING_REGEX, ''); +} + +function cleanEndingSeparator(path) { + return path.replace(PATH_SEPARATOR_ENDING_REGEX, ''); +} + +/** + * Safely joins the given paths which might both start and end with a `/` + * + * Example: + * - `joinPaths('abc/', '/def') === 'abc/def'` + * - `joinPaths(null, 'abc/def', 'zoo) === 'abc/def/zoo'` + * + * @param {...String} paths + * @returns {String} + */ +export function joinPaths(...paths) { + return paths.reduce((acc, path) => { + if (!path) { + return acc; + } + if (!acc) { + return path; + } + + return [cleanEndingSeparator(acc), PATH_SEPARATOR, cleanLeadingSeparator(path)].join(''); + }, ''); +} + // Returns an array containing the value(s) of the // of the key passed as an argument export function getParameterValues(sParam, url = window.location) { @@ -181,4 +214,71 @@ export function getWebSocketUrl(path) { return `${getWebSocketProtocol()}//${joinPaths(window.location.host, path)}`; } -export { joinPaths }; +/** + * Convert search query into an object + * + * @param {String} query from "document.location.search" + * @returns {Object} + * + * ex: "?one=1&two=2" into {one: 1, two: 2} + */ +export function queryToObject(query) { + const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query; + return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => { + const p = curr.split('='); + accumulator[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); + return accumulator; + }, {}); +} + +/** + * Convert search query object back into a search query + * + * @param {Object} obj that needs to be converted + * @returns {String} + * + * ex: {one: 1, two: 2} into "one=1&two=2" + * + */ +export function objectToQuery(obj) { + return Object.keys(obj) + .map(k => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`) + .join('&'); +} + +/** + * Sets query params for a given URL + * It adds new query params, updates existing params with a new value and removes params with value null/undefined + * + * @param {Object} params The query params to be set/updated + * @param {String} url The url to be operated on + * @param {Boolean} clearParams Indicates whether existing query params should be removed or not + * @returns {String} A copy of the original with the updated query params + */ +export const setUrlParams = (params, url = window.location.href, clearParams = false) => { + const urlObj = new URL(url); + const queryString = urlObj.search; + const searchParams = clearParams ? new URLSearchParams('') : new URLSearchParams(queryString); + + Object.keys(params).forEach(key => { + if (params[key] === null || params[key] === undefined) { + searchParams.delete(key); + } else if (Array.isArray(params[key])) { + params[key].forEach((val, idx) => { + if (idx === 0) { + searchParams.set(key, val); + } else { + searchParams.append(key, val); + } + }); + } else { + searchParams.set(key, params[key]); + } + }); + + urlObj.search = searchParams.toString(); + + return urlObj.toString(); +}; + +export const escapeFileUrl = fileUrl => encodeURIComponent(fileUrl).replace(/%2F/g, '/'); -- cgit v1.2.1