From 85dc423f7090da0a52c73eb66faf22ddb20efff9 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Sat, 19 Sep 2020 01:45:44 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-4-stable-ee --- app/assets/javascripts/lib/graphql.js | 33 ++++++++-- .../javascripts/lib/utils/axios_startup_calls.js | 73 ++++++++++++-------- .../javascripts/lib/utils/datetime_utility.js | 38 ++++++++--- app/assets/javascripts/lib/utils/forms.js | 37 +++++++++++ app/assets/javascripts/lib/utils/image_utility.js | 2 - app/assets/javascripts/lib/utils/jquery_at_who.js | 3 + app/assets/javascripts/lib/utils/poll.js | 6 +- app/assets/javascripts/lib/utils/set.js | 1 - app/assets/javascripts/lib/utils/simple_poll.js | 4 +- app/assets/javascripts/lib/utils/text_markdown.js | 77 ++++++++++++++++++---- app/assets/javascripts/lib/utils/text_utility.js | 75 +++++++++++++++++++++ app/assets/javascripts/lib/utils/type_utility.js | 1 - app/assets/javascripts/lib/utils/url_utility.js | 27 ++++++-- app/assets/javascripts/lib/utils/webpack.js | 1 - 14 files changed, 312 insertions(+), 66 deletions(-) create mode 100644 app/assets/javascripts/lib/utils/jquery_at_who.js (limited to 'app/assets/javascripts/lib') diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index 4fed121779e..d2907f401c0 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -4,6 +4,7 @@ import { createUploadLink } from 'apollo-upload-client'; import { ApolloLink } from 'apollo-link'; import { BatchHttpLink } from 'apollo-link-batch-http'; import csrf from '~/lib/utils/csrf'; +import PerformanceBarService from '~/performance_bar/services/performance_bar_service'; export const fetchPolicies = { CACHE_FIRST: 'cache-first', @@ -32,13 +33,35 @@ export default (resolvers = {}, config = {}) => { credentials: 'same-origin', }; + const uploadsLink = ApolloLink.split( + operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest, + createUploadLink(httpOptions), + new BatchHttpLink(httpOptions), + ); + + const performanceBarLink = new ApolloLink((operation, forward) => { + return forward(operation).map(response => { + const httpResponse = operation.getContext().response; + + if (PerformanceBarService.interceptor) { + PerformanceBarService.interceptor({ + config: { + url: httpResponse.url, + }, + headers: { + 'x-request-id': httpResponse.headers.get('x-request-id'), + 'x-gitlab-from-cache': httpResponse.headers.get('x-gitlab-from-cache'), + }, + }); + } + + return response; + }); + }); + return new ApolloClient({ typeDefs: config.typeDefs, - link: ApolloLink.split( - operation => operation.getContext().hasUpload || operation.getContext().isSingleRequest, - createUploadLink(httpOptions), - new BatchHttpLink(httpOptions), - ), + link: ApolloLink.from([performanceBarLink, uploadsLink]), cache: new InMemoryCache({ ...config.cacheConfig, freezeResults: config.assumeImmutableResults, diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js index a047cebc8ab..7e2665b910c 100644 --- a/app/assets/javascripts/lib/utils/axios_startup_calls.js +++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js @@ -10,6 +10,32 @@ const getFullUrl = req => { return mergeUrlParams(req.params || {}, url); }; +const handleStartupCall = async ({ fetchCall }, req) => { + const res = await fetchCall; + if (!res.ok) { + throw new Error(res.statusText); + } + + const fetchHeaders = {}; + res.headers.forEach((val, key) => { + fetchHeaders[key] = val; + }); + + const data = await res.clone().json(); + + Object.assign(req, { + adapter: () => + Promise.resolve({ + data, + status: res.status, + statusText: res.statusText, + headers: fetchHeaders, + config: req, + request: req, + }), + }); +}; + const setupAxiosStartupCalls = axios => { const { startup_calls: startupCalls } = window.gl || {}; @@ -17,35 +43,28 @@ const setupAxiosStartupCalls = axios => { return; } - // TODO: To save performance of future axios calls, we can - // remove this interceptor once the "startupCalls" have been loaded - axios.interceptors.request.use(req => { + const remainingCalls = new Map(Object.entries(startupCalls)); + + const interceptor = axios.interceptors.request.use(async req => { const fullUrl = getFullUrl(req); - const existing = startupCalls[fullUrl]; - - if (existing) { - // eslint-disable-next-line no-param-reassign - req.adapter = () => - existing.fetchCall.then(res => { - const fetchHeaders = {}; - res.headers.forEach((val, key) => { - fetchHeaders[key] = val; - }); - - // eslint-disable-next-line promise/no-nesting - return res - .clone() - .json() - .then(data => ({ - data, - status: res.status, - statusText: res.statusText, - headers: fetchHeaders, - config: req, - request: req, - })); - }); + const startupCall = remainingCalls.get(fullUrl); + + if (!startupCall?.fetchCall) { + return req; + } + + try { + await handleStartupCall(startupCall, req); + } catch (e) { + // eslint-disable-next-line no-console + console.warn(`[gitlab] Something went wrong with the startup call for "${fullUrl}"`, e); + } + + remainingCalls.delete(fullUrl); + + if (remainingCalls.size === 0) { + axios.interceptors.request.eject(interceptor); } return req; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index e26b63fbb85..b193a8b2c9a 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -216,8 +216,9 @@ export const timeFor = (time, expiredLabel) => { return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim(); }; +export const millisecondsPerDay = 1000 * 60 * 60 * 24; + export const getDayDifference = (a, b) => { - const millisecondsPerDay = 1000 * 60 * 60 * 24; const date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); const date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); @@ -641,6 +642,16 @@ export const secondsToMilliseconds = seconds => seconds * 1000; */ export const secondsToDays = seconds => Math.round(seconds / 86400); +/** + * Returns the date n days after the date provided + * + * @param {Date} date the initial date + * @param {Number} numberOfDays number of days after + * @return {Date} the date following the date provided + */ +export const nDaysAfter = (date, numberOfDays) => + new Date(newDate(date)).setDate(date.getDate() + numberOfDays); + /** * Returns the date after the date provided * @@ -702,20 +713,14 @@ export const approximateDuration = (seconds = 0) => { * @return {Date} the date object from the params */ export const dateFromParams = (year, month, day) => { - const date = new Date(); - - date.setFullYear(year); - date.setMonth(month); - date.setDate(day); - - return date; + return new Date(year, month, day); }; /** * A utility function which computes the difference in seconds * between 2 dates. * - * @param {Date} startDate the start sate + * @param {Date} startDate the start date * @param {Date} endDate the end date * * @return {Int} the difference in seconds @@ -723,3 +728,18 @@ export const dateFromParams = (year, month, day) => { export const differenceInSeconds = (startDate, endDate) => { return (endDate.getTime() - startDate.getTime()) / 1000; }; + +/** + * A utility function which computes the difference in milliseconds + * between 2 dates. + * + * @param {Date|Int} startDate the start date. Can be either a date object or a unix timestamp. + * @param {Date|Int} endDate the end date. Can be either a date object or a unix timestamp. Defaults to now. + * + * @return {Int} the difference in milliseconds + */ +export const differenceInMilliseconds = (startDate, endDate = Date.now()) => { + const startDateInMS = startDate instanceof Date ? startDate.getTime() : startDate; + const endDateInMS = endDate instanceof Date ? endDate.getTime() : endDate; + return endDateInMS - startDateInMS; +}; diff --git a/app/assets/javascripts/lib/utils/forms.js b/app/assets/javascripts/lib/utils/forms.js index ced44ab9817..1c5f6cefeda 100644 --- a/app/assets/javascripts/lib/utils/forms.js +++ b/app/assets/javascripts/lib/utils/forms.js @@ -14,3 +14,40 @@ export const serializeForm = form => { return serializeFormEntries(entries); }; + +/** + * Check if the value provided is empty or not + * + * It is being used to check if a form input + * value has been set or not + * + * @param {String, Number, Array} - Any form value + * @returns {Boolean} - returns false if a value is set + * + * @example + * returns true for '', [], null, undefined + */ +export const isEmptyValue = value => value == null || value.length === 0; + +/** + * A form object serializer + * + * @param {Object} - Form Object + * @returns {Object} - Serialized Form Object + * + * @example + * Input + * {"project": {"value": "hello", "state": false}, "username": {"value": "john"}} + * + * Returns + * {"project": "hello", "username": "john"} + */ +export const serializeFormObject = form => + Object.fromEntries( + Object.entries(form).reduce((acc, [name, { value }]) => { + if (!isEmptyValue(value)) { + acc.push([name, value]); + } + return acc; + }, []), + ); diff --git a/app/assets/javascripts/lib/utils/image_utility.js b/app/assets/javascripts/lib/utils/image_utility.js index 2977ec821cb..af531f9f830 100644 --- a/app/assets/javascripts/lib/utils/image_utility.js +++ b/app/assets/javascripts/lib/utils/image_utility.js @@ -1,5 +1,3 @@ -/* eslint-disable import/prefer-default-export */ - export function isImageLoaded(element) { return element.complete && element.naturalHeight !== 0; } diff --git a/app/assets/javascripts/lib/utils/jquery_at_who.js b/app/assets/javascripts/lib/utils/jquery_at_who.js new file mode 100644 index 00000000000..88111cb4ae4 --- /dev/null +++ b/app/assets/javascripts/lib/utils/jquery_at_who.js @@ -0,0 +1,3 @@ +import 'jquery'; +import 'jquery.caret'; // required by at.js +import '@gitlab/at.js'; diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index 7f0c65868c2..e8583fa951b 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -88,7 +88,11 @@ export default class Poll { } makeDelayedRequest(delay = 0) { - this.timeoutID = setTimeout(() => this.makeRequest(), delay); + // So we don't make our specs artificially slower + this.timeoutID = setTimeout( + () => this.makeRequest(), + process.env.NODE_ENV !== 'test' ? delay : 1, + ); } makeRequest() { diff --git a/app/assets/javascripts/lib/utils/set.js b/app/assets/javascripts/lib/utils/set.js index 3845d648b61..541934c4221 100644 --- a/app/assets/javascripts/lib/utils/set.js +++ b/app/assets/javascripts/lib/utils/set.js @@ -4,6 +4,5 @@ * @param {Set} superset The set to be considered as the superset. * @returns {boolean} */ -// eslint-disable-next-line import/prefer-default-export export const isSubset = (subset, superset) => Array.from(subset).every(value => superset.has(value)); diff --git a/app/assets/javascripts/lib/utils/simple_poll.js b/app/assets/javascripts/lib/utils/simple_poll.js index 576a9ec880c..e4e9fb2e6fa 100644 --- a/app/assets/javascripts/lib/utils/simple_poll.js +++ b/app/assets/javascripts/lib/utils/simple_poll.js @@ -1,10 +1,12 @@ +import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; + export default (fn, { interval = 2000, timeout = 60000 } = {}) => { const startTime = Date.now(); return new Promise((resolve, reject) => { const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg)); const next = () => { - if (timeout === 0 || Date.now() - startTime < timeout) { + if (timeout === 0 || differenceInMilliseconds(startTime) < timeout) { setTimeout(fn.bind(null, next, stop), interval); } else { reject(new Error('SIMPLE_POLL_TIMEOUT')); diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 8d23d177410..f4c6e4e3584 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, no-param-reassign, operator-assignment, consistent-return */ import $ from 'jquery'; import { insertText } from '~/lib/utils/common_utils'; +import Shortcuts from '~/behaviors/shortcuts/shortcuts'; const LINK_TAG_PATTERN = '[{text}](url)'; @@ -303,23 +304,67 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo }); } +/* eslint-disable @gitlab/require-i18n-strings */ +export function keypressNoteText(e) { + if (this.selectionStart === this.selectionEnd) { + return; + } + const keys = { + '*': '**{text}**', // wraps with bold character + _: '_{text}_', // wraps with italic character + '`': '`{text}`', // wraps with inline character + "'": "'{text}'", // single quotes + '"': '"{text}"', // double quotes + '[': '[{text}]', // brackets + '{': '{{text}}', // braces + '(': '({text})', // parentheses + '<': '<{text}>', // angle brackets + }; + const tag = keys[e.key]; + + if (tag) { + e.preventDefault(); + + updateText({ + tag, + textArea: this, + blockTag: '', + wrap: true, + select: '', + tagContent: '', + }); + } +} +/* eslint-enable @gitlab/require-i18n-strings */ + +export function updateTextForToolbarBtn($toolbarBtn) { + return updateText({ + textArea: $toolbarBtn.closest('.md-area').find('textarea'), + tag: $toolbarBtn.data('mdTag'), + cursorOffset: $toolbarBtn.data('mdCursorOffset'), + blockTag: $toolbarBtn.data('mdBlock'), + wrap: !$toolbarBtn.data('mdPrepend'), + select: $toolbarBtn.data('mdSelect'), + tagContent: $toolbarBtn.data('mdTagContent'), + }); +} + export function addMarkdownListeners(form) { - return $('.js-md', form) + $('.markdown-area', form) + .on('keydown', keypressNoteText) + .each(function attachTextareaShortcutHandlers() { + Shortcuts.initMarkdownEditorShortcuts($(this), updateTextForToolbarBtn); + }); + + const $allToolbarBtns = $('.js-md', form) .off('click') .on('click', function() { - const $this = $(this); - const tag = this.dataset.mdTag; - - return updateText({ - textArea: $this.closest('.md-area').find('textarea'), - tag, - cursorOffset: $this.data('mdCursorOffset'), - blockTag: $this.data('mdBlock'), - wrap: !$this.data('mdPrepend'), - select: $this.data('mdSelect'), - tagContent: $this.data('mdTagContent'), - }); + const $toolbarBtn = $(this); + + return updateTextForToolbarBtn($toolbarBtn); }); + + return $allToolbarBtns; } export function addEditorMarkdownListeners(editor) { @@ -342,5 +387,11 @@ export function addEditorMarkdownListeners(editor) { } export function removeMarkdownListeners(form) { + $('.markdown-area', form) + .off('keydown', keypressNoteText) + .each(function removeTextareaShortcutHandlers() { + Shortcuts.removeMarkdownEditorShortcuts($(this)); + }); + return $('.js-md', form).off('click'); } diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index e2953ce330c..8ac6a44cba9 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -275,6 +275,81 @@ export const convertToSentenceCase = string => { */ export const convertToTitleCase = string => string.replace(/\b[a-z]/g, s => s.toUpperCase()); +const unicodeConversion = [ + [/[ÀÁÂÃÅĀĂĄ]/g, 'A'], + [/[Æ]/g, 'AE'], + [/[ÇĆĈĊČ]/g, 'C'], + [/[ÈÉÊËĒĔĖĘĚ]/g, 'E'], + [/[ÌÍÎÏĨĪĬĮİ]/g, 'I'], + [/[Ððĥħ]/g, 'h'], + [/[ÑŃŅŇʼn]/g, 'N'], + [/[ÒÓÔÕØŌŎŐ]/g, 'O'], + [/[ÙÚÛŨŪŬŮŰŲ]/g, 'U'], + [/[ÝŶŸ]/g, 'Y'], + [/[Þñþńņň]/g, 'n'], + [/[ߌŜŞŠ]/g, 'S'], + [/[àáâãåāăąĸ]/g, 'a'], + [/[æ]/g, 'ae'], + [/[çćĉċč]/g, 'c'], + [/[èéêëēĕėęě]/g, 'e'], + [/[ìíîïĩīĭį]/g, 'i'], + [/[òóôõøōŏő]/g, 'o'], + [/[ùúûũūŭůűų]/g, 'u'], + [/[ýÿŷ]/g, 'y'], + [/[ĎĐ]/g, 'D'], + [/[ďđ]/g, 'd'], + [/[ĜĞĠĢ]/g, 'G'], + [/[ĝğġģŊŋſ]/g, 'g'], + [/[ĤĦ]/g, 'H'], + [/[ıśŝşš]/g, 's'], + [/[IJ]/g, 'IJ'], + [/[ij]/g, 'ij'], + [/[Ĵ]/g, 'J'], + [/[ĵ]/g, 'j'], + [/[Ķ]/g, 'K'], + [/[ķ]/g, 'k'], + [/[ĹĻĽĿŁ]/g, 'L'], + [/[ĺļľŀł]/g, 'l'], + [/[Œ]/g, 'OE'], + [/[œ]/g, 'oe'], + [/[ŔŖŘ]/g, 'R'], + [/[ŕŗř]/g, 'r'], + [/[ŢŤŦ]/g, 'T'], + [/[ţťŧ]/g, 't'], + [/[Ŵ]/g, 'W'], + [/[ŵ]/g, 'w'], + [/[ŹŻŽ]/g, 'Z'], + [/[źżž]/g, 'z'], + [/ö/g, 'oe'], + [/ü/g, 'ue'], + [/ä/g, 'ae'], + // eslint-disable-next-line @gitlab/require-i18n-strings + [/Ö/g, 'Oe'], + // eslint-disable-next-line @gitlab/require-i18n-strings + [/Ü/g, 'Ue'], + // eslint-disable-next-line @gitlab/require-i18n-strings + [/Ä/g, 'Ae'], +]; + +/** + * Converts each non-ascii character in a string to + * it's ascii equivalent (if defined). + * + * e.g. "Dĭd söméònê äšk fœŕ Ůnĭċődę?" => "Did someone aesk foer Unicode?" + * + * @param {String} string + * @returns {String} + */ +export const convertUnicodeToAscii = string => { + let convertedString = string; + + unicodeConversion.forEach(([regex, replacer]) => { + convertedString = convertedString.replace(regex, replacer); + }); + + return convertedString; +}; + /** * Splits camelCase or PascalCase words * e.g. HelloWorld => Hello World diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js index be86f336bcd..664c0dbbc84 100644 --- a/app/assets/javascripts/lib/utils/type_utility.js +++ b/app/assets/javascripts/lib/utils/type_utility.js @@ -1,2 +1 @@ -// eslint-disable-next-line import/prefer-default-export export const isObject = obj => obj && obj.constructor === Object; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 8077570158a..e9c3fe0a406 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -75,7 +75,7 @@ export function getParameterValues(sParam, url = window.location) { * @param {Boolean} options.spreadArrays - split array values into separate key/value-pairs */ export function mergeUrlParams(params, url, options = {}) { - const { spreadArrays = false } = options; + const { spreadArrays = false, sort = false } = options; const re = /^([^?#]*)(\?[^#]*)?(.*)/; let merged = {}; const [, fullpath, query, fragment] = url.match(re); @@ -108,7 +108,9 @@ export function mergeUrlParams(params, url, options = {}) { Object.assign(merged, params); - const newQuery = Object.keys(merged) + const mergedKeys = sort ? Object.keys(merged).sort() : Object.keys(merged); + + const newQuery = mergedKeys .filter(key => merged[key] !== null) .map(key => { let value = merged[key]; @@ -334,17 +336,32 @@ export function getWebSocketUrl(path) { * Convert search query into an object * * @param {String} query from "document.location.search" + * @param {Object} options + * @param {Boolean} options.gatherArrays - gather array values into an Array * @returns {Object} * * ex: "?one=1&two=2" into {one: 1, two: 2} */ -export function queryToObject(query) { +export function queryToObject(query, options = {}) { + const { gatherArrays = false } = options; const removeQuestionMarkFromQuery = String(query).startsWith('?') ? query.slice(1) : query; return removeQuestionMarkFromQuery.split('&').reduce((accumulator, curr) => { const [key, value] = curr.split('='); - if (value !== undefined) { - accumulator[decodeURIComponent(key)] = decodeURIComponent(value); + if (value === undefined) { + return accumulator; } + const decodedValue = decodeURIComponent(value); + + if (gatherArrays && key.endsWith('[]')) { + const decodedKey = decodeURIComponent(key.slice(0, -2)); + if (!Array.isArray(accumulator[decodedKey])) { + accumulator[decodedKey] = []; + } + accumulator[decodedKey].push(decodedValue); + } else { + accumulator[decodeURIComponent(key)] = decodedValue; + } + return accumulator; }, {}); } diff --git a/app/assets/javascripts/lib/utils/webpack.js b/app/assets/javascripts/lib/utils/webpack.js index 390294afcb7..622c40e0f35 100644 --- a/app/assets/javascripts/lib/utils/webpack.js +++ b/app/assets/javascripts/lib/utils/webpack.js @@ -1,7 +1,6 @@ import { joinPaths } from '~/lib/utils/url_utility'; // tell webpack to load assets from origin so that web workers don't break -// eslint-disable-next-line import/prefer-default-export export function resetServiceWorkersPublicPath() { // __webpack_public_path__ is a global variable that can be used to adjust // the webpack publicPath setting at runtime. -- cgit v1.2.1