diff options
Diffstat (limited to 'app/assets/javascripts/lib')
-rw-r--r-- | app/assets/javascripts/lib/chrome_84_icon_fix.js | 78 | ||||
-rw-r--r-- | app/assets/javascripts/lib/graphql.js | 2 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/axios_startup_calls.js | 19 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/common_utils.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/datetime_utility.js | 34 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/highlight.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/http_status.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/keys.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/poll.js | 17 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/text_markdown.js | 4 | ||||
-rw-r--r-- | app/assets/javascripts/lib/utils/url_utility.js | 41 |
11 files changed, 184 insertions, 24 deletions
diff --git a/app/assets/javascripts/lib/chrome_84_icon_fix.js b/app/assets/javascripts/lib/chrome_84_icon_fix.js new file mode 100644 index 00000000000..60497186c19 --- /dev/null +++ b/app/assets/javascripts/lib/chrome_84_icon_fix.js @@ -0,0 +1,78 @@ +import { debounce } from 'lodash'; + +/* + Chrome and Edge 84 have a bug relating to icon sprite svgs + https://bugs.chromium.org/p/chromium/issues/detail?id=1107442 + + If the SVG is loaded, under certain circumstances the icons are not + shown. We load our sprite icons with JS and add them to the body. + Then we iterate over all the `use` elements and replace their reference + to that svg which we added internally. In order to avoid id conflicts, + those are renamed with a unique prefix. + + We do that once the DOMContentLoaded fired and otherwise we use a + mutation observer to re-trigger this logic. + + In order to not have a big impact on performance or to cause flickering + of of content, + + 1. we only do it for each svg once + 2. we debounce the event handler and just do it in a requestIdleCallback + + Before we tried to do it with the library svg4everybody and it had a big + performance impact. See: + https://gitlab.com/gitlab-org/quality/performance/-/issues/312 + */ +document.addEventListener('DOMContentLoaded', async () => { + const GITLAB_SVG_PREFIX = 'chrome-issue-230433-gitlab-svgs'; + const FILE_ICON_PREFIX = 'chrome-issue-230433-file-icons'; + const SKIP_ATTRIBUTE = 'data-replaced-by-chrome-issue-230433'; + + const fixSVGs = () => { + requestIdleCallback(() => { + document.querySelectorAll(`use:not([${SKIP_ATTRIBUTE}])`).forEach(use => { + const href = use?.getAttribute('href') ?? use?.getAttribute('xlink:href') ?? ''; + + if (href.includes(window.gon.sprite_icons)) { + use.removeAttribute('xlink:href'); + use.setAttribute('href', `#${GITLAB_SVG_PREFIX}-${href.split('#')[1]}`); + } else if (href.includes(window.gon.sprite_file_icons)) { + use.removeAttribute('xlink:href'); + use.setAttribute('href', `#${FILE_ICON_PREFIX}-${href.split('#')[1]}`); + } + + use.setAttribute(SKIP_ATTRIBUTE, 'true'); + }); + }); + }; + + const watchForNewSVGs = () => { + const observer = new MutationObserver(debounce(fixSVGs, 200)); + observer.observe(document.querySelector('body'), { + childList: true, + attributes: false, + subtree: true, + }); + }; + + const retrieveIconSprites = async (url, prefix) => { + const div = document.createElement('div'); + div.classList.add('hidden'); + const result = await fetch(url); + div.innerHTML = await result.text(); + div.querySelectorAll('[id]').forEach(node => { + node.setAttribute('id', `${prefix}-${node.getAttribute('id')}`); + }); + document.body.append(div); + }; + + if (window.gon && window.gon.sprite_icons) { + await retrieveIconSprites(window.gon.sprite_icons, GITLAB_SVG_PREFIX); + if (window.gon.sprite_file_icons) { + await retrieveIconSprites(window.gon.sprite_file_icons, FILE_ICON_PREFIX); + } + + fixSVGs(); + watchForNewSVGs(); + } +}); diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index b6c41ffa7ab..4fed121779e 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -14,7 +14,7 @@ export const fetchPolicies = { }; export default (resolvers = {}, config = {}) => { - let uri = `${gon.relative_url_root}/api/graphql`; + let uri = `${gon.relative_url_root || ''}/api/graphql`; if (config.baseUrl) { // Prepend baseUrl and ensure that `///` are replaced with `/` diff --git a/app/assets/javascripts/lib/utils/axios_startup_calls.js b/app/assets/javascripts/lib/utils/axios_startup_calls.js index cb2e8a76c08..a047cebc8ab 100644 --- a/app/assets/javascripts/lib/utils/axios_startup_calls.js +++ b/app/assets/javascripts/lib/utils/axios_startup_calls.js @@ -34,14 +34,17 @@ const setupAxiosStartupCalls = axios => { }); // eslint-disable-next-line promise/no-nesting - return res.json().then(data => ({ - data, - status: res.status, - statusText: res.statusText, - headers: fetchHeaders, - config: req, - request: req, - })); + return res + .clone() + .json() + .then(data => ({ + data, + status: res.status, + statusText: res.statusText, + headers: fetchHeaders, + config: req, + request: req, + })); }); } diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 8bf9a281151..bcf302cc262 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -4,12 +4,12 @@ import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils'; import $ from 'jquery'; +import { isFunction } from 'lodash'; +import Cookies from 'js-cookie'; import axios from './axios_utils'; import { getLocationHash } from './url_utility'; import { convertToCamelCase, convertToSnakeCase } from './text_utility'; import { isObject } from './type_utility'; -import { isFunction } from 'lodash'; -import Cookies from 'js-cookie'; export const getPagePath = (index = 0) => { const page = $('body').attr('data-page') || ''; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 6e02fc1eb91..e26b63fbb85 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -689,3 +689,37 @@ export const approximateDuration = (seconds = 0) => { } return n__('1 day', '%d days', seconds < ONE_DAY_LIMIT ? 1 : days); }; + +/** + * A utility function which helps creating a date object + * for a specific date. Accepts the year, month and day + * returning a date object for the given params. + * + * @param {Int} year the full year as a number i.e. 2020 + * @param {Int} month the month index i.e. January => 0 + * @param {Int} day the day as a number i.e. 23 + * + * @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; +}; + +/** + * A utility function which computes the difference in seconds + * between 2 dates. + * + * @param {Date} startDate the start sate + * @param {Date} endDate the end date + * + * @return {Int} the difference in seconds + */ +export const differenceInSeconds = (startDate, endDate) => { + return (endDate.getTime() - startDate.getTime()) / 1000; +}; diff --git a/app/assets/javascripts/lib/utils/highlight.js b/app/assets/javascripts/lib/utils/highlight.js index b1dd562f63a..32553af9af3 100644 --- a/app/assets/javascripts/lib/utils/highlight.js +++ b/app/assets/javascripts/lib/utils/highlight.js @@ -1,5 +1,5 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import sanitize from 'sanitize-html'; +import { sanitize } from 'dompurify'; /** * Wraps substring matches with HTML `<span>` elements. @@ -24,7 +24,7 @@ export default function highlight(string, match = '', matchPrefix = '<b>', match return string; } - const sanitizedValue = sanitize(string.toString(), { allowedTags: [] }); + const sanitizedValue = sanitize(string.toString(), { ALLOWED_TAGS: [] }); // occurrences is an array of character indices that should be // highlighted in the original string, i.e. [3, 4, 5, 7] diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index 08a77966bbd..7132986a7e6 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -22,6 +22,7 @@ const httpStatusCodes = { CONFLICT: 409, GONE: 410, UNPROCESSABLE_ENTITY: 422, + INTERNAL_SERVER_ERROR: 500, SERVICE_UNAVAILABLE: 503, }; diff --git a/app/assets/javascripts/lib/utils/keys.js b/app/assets/javascripts/lib/utils/keys.js index 8e5420e87ea..2a8b1759e54 100644 --- a/app/assets/javascripts/lib/utils/keys.js +++ b/app/assets/javascripts/lib/utils/keys.js @@ -1,4 +1,2 @@ -/* eslint-disable @gitlab/require-i18n-strings */ - export const ESC_KEY = 'Escape'; -export const ESC_KEY_IE11 = 'Esc'; // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key +export const ENTER_KEY = 'Enter'; diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index a900ff34bf5..7f0c65868c2 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -46,6 +46,19 @@ import { normalizeHeaders } from './common_utils'; * 4. If HTTP response is 200, we poll. * 5. If HTTP response is different from 200, we stop polling. * + * @example + * // With initial delay (for, for example, reducing unnecessary requests) + * + * const poll = new Poll({ + * resource: this.service, + * method: 'fetchNotes', + * successCallback: () => {}, + * errorCallback: () => {}, + * }); + * + * // Performs the first request in 2.5s and then uses the `Poll-Interval` header. + * poll.makeDelayedRequest(2500); + * */ export default class Poll { constructor(options = {}) { @@ -74,6 +87,10 @@ export default class Poll { this.options.successCallback(response); } + makeDelayedRequest(delay = 0) { + this.timeoutID = setTimeout(() => this.makeRequest(), delay); + } + makeRequest() { const { resource, method, data, errorCallback, notificationCallback } = this.options; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 4d25ee9e4bd..8d23d177410 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -308,9 +308,11 @@ export function addMarkdownListeners(form) { .off('click') .on('click', function() { const $this = $(this); + const tag = this.dataset.mdTag; + return updateText({ textArea: $this.closest('.md-area').find('textarea'), - tag: $this.data('mdTag'), + tag, cursorOffset: $this.data('mdCursorOffset'), blockTag: $this.data('mdBlock'), wrap: !$this.data('mdPrepend'), diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index c6c34b831ee..8077570158a 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -71,29 +71,56 @@ export function getParameterValues(sParam, url = window.location) { * * @param {Object} params - url keys and value to merge * @param {String} url + * @param {Object} options + * @param {Boolean} options.spreadArrays - split array values into separate key/value-pairs */ -export function mergeUrlParams(params, url) { +export function mergeUrlParams(params, url, options = {}) { + const { spreadArrays = false } = options; const re = /^([^?#]*)(\?[^#]*)?(.*)/; - const merged = {}; + let merged = {}; const [, fullpath, query, fragment] = url.match(re); if (query) { - query + merged = query .substr(1) .split('&') - .forEach(part => { + .reduce((memo, part) => { if (part.length) { const kv = part.split('='); - merged[decodeUrlParameter(kv[0])] = decodeUrlParameter(kv.slice(1).join('=')); + let key = decodeUrlParameter(kv[0]); + const value = decodeUrlParameter(kv.slice(1).join('=')); + if (spreadArrays && key.endsWith('[]')) { + key = key.slice(0, -2); + if (!Array.isArray(memo[key])) { + return { ...memo, [key]: [value] }; + } + memo[key].push(value); + + return memo; + } + + return { ...memo, [key]: value }; } - }); + + return memo; + }, {}); } Object.assign(merged, params); const newQuery = Object.keys(merged) .filter(key => merged[key] !== null) - .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(merged[key])}`) + .map(key => { + let value = merged[key]; + const encodedKey = encodeURIComponent(key); + if (spreadArrays && Array.isArray(value)) { + value = merged[key] + .map(arrayValue => encodeURIComponent(arrayValue)) + .join(`&${encodedKey}[]=`); + return `${encodedKey}[]=${value}`; + } + return `${encodedKey}=${encodeURIComponent(value)}`; + }) .join('&'); if (newQuery) { |