From ee664acb356f8123f4f6b00b73c1e1cf0866c7fb Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 20 Oct 2022 09:40:42 +0000 Subject: Add latest changes from gitlab-org/gitlab@15-5-stable-ee --- app/assets/javascripts/lib/dompurify.js | 13 ++++- app/assets/javascripts/lib/utils/autosave.js | 40 ++++++++++++-- app/assets/javascripts/lib/utils/common_utils.js | 51 +++++++++--------- .../lib/utils/datetime/date_calculation_utility.js | 18 ------- .../lib/utils/datetime/date_format_utility.js | 26 +++++++++ app/assets/javascripts/lib/utils/text_markdown.js | 61 ++++++++++------------ app/assets/javascripts/lib/utils/text_utility.js | 14 ++++- 7 files changed, 139 insertions(+), 84 deletions(-) (limited to 'app/assets/javascripts/lib') diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js index 6f24590f9e7..27760e483aa 100644 --- a/app/assets/javascripts/lib/dompurify.js +++ b/app/assets/javascripts/lib/dompurify.js @@ -3,12 +3,21 @@ import { getNormalizedURL, getBaseURL, relativePathToAbsolute } from '~/lib/util const { sanitize: dompurifySanitize, addHook, isValidAttribute } = DOMPurify; -const defaultConfig = { +export const defaultConfig = { // Safely allow SVG tags ADD_TAGS: ['use', 'gl-emoji', 'copy-code'], // Prevent possible XSS attacks with data-* attributes used by @rails/ujs // See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1421 - FORBID_ATTR: ['data-remote', 'data-url', 'data-type', 'data-method'], + FORBID_ATTR: [ + 'data-remote', + 'data-url', + 'data-type', + 'data-method', + 'data-disable-with', + 'data-disabled', + 'data-disable', + 'data-turbo', + ], FORBID_TAGS: ['style', 'mstyle'], ALLOW_UNKNOWN_PROTOCOLS: true, }; diff --git a/app/assets/javascripts/lib/utils/autosave.js b/app/assets/javascripts/lib/utils/autosave.js index dac1da743a2..01316be06a2 100644 --- a/app/assets/javascripts/lib/utils/autosave.js +++ b/app/assets/javascripts/lib/utils/autosave.js @@ -1,8 +1,27 @@ +import { isString } from 'lodash'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +const normalizeKey = (autosaveKey) => { + let normalizedKey; + + if (Array.isArray(autosaveKey) && autosaveKey.every(isString)) { + normalizedKey = autosaveKey.join('/'); + } else if (isString(autosaveKey)) { + normalizedKey = autosaveKey; + } else { + // eslint-disable-next-line @gitlab/require-i18n-strings + throw new Error('Invalid autosave key'); + } + + return `autosave/${normalizedKey}`; +}; + +const lockVersionKey = (autosaveKey) => `${normalizeKey(autosaveKey)}/lockVersion`; + export const clearDraft = (autosaveKey) => { try { - window.localStorage.removeItem(`autosave/${autosaveKey}`); + window.localStorage.removeItem(normalizeKey(autosaveKey)); + window.localStorage.removeItem(lockVersionKey(autosaveKey)); } catch (e) { // eslint-disable-next-line no-console console.error(e); @@ -11,7 +30,17 @@ export const clearDraft = (autosaveKey) => { export const getDraft = (autosaveKey) => { try { - return window.localStorage.getItem(`autosave/${autosaveKey}`); + return window.localStorage.getItem(normalizeKey(autosaveKey)); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + return null; + } +}; + +export const getLockVersion = (autosaveKey) => { + try { + return window.localStorage.getItem(lockVersionKey(autosaveKey)); } catch (e) { // eslint-disable-next-line no-console console.error(e); @@ -19,9 +48,12 @@ export const getDraft = (autosaveKey) => { } }; -export const updateDraft = (autosaveKey, text) => { +export const updateDraft = (autosaveKey, text, lockVersion) => { try { - window.localStorage.setItem(`autosave/${autosaveKey}`, text); + window.localStorage.setItem(normalizeKey(autosaveKey), text); + if (lockVersion) { + window.localStorage.setItem(lockVersionKey(autosaveKey), lockVersion); + } } catch (e) { // eslint-disable-next-line no-console console.error(e); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 7925a10344a..4448a106bb6 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -60,6 +60,15 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa }); }; +/** + * Return the given element's offset height, or 0 if the element doesn't exist. + * Probably not useful outside of handleLocationHash. + * + * @param {HTMLElement} element The element to measure. + * @returns {number} The element's offset height. + */ +const getElementOffsetHeight = (element) => element?.offsetHeight ?? 0; + // automatically adjust scroll position for hash urls taking the height of the navbar into account // https://github.com/twitter/bootstrap/issues/1768 export const handleLocationHash = () => { @@ -84,40 +93,26 @@ export const handleLocationHash = () => { const fixedIssuableTitle = document.querySelector('.issue-sticky-header'); let adjustment = 0; - if (fixedNav) adjustment -= fixedNav.offsetHeight; - - if (target && target.scrollIntoView) { - target.scrollIntoView(true); - } - if (fixedTabs) { - adjustment -= fixedTabs.offsetHeight; - } - - if (fixedDiffStats) { - adjustment -= fixedDiffStats.offsetHeight; - } - - if (performanceBar) { - adjustment -= performanceBar.offsetHeight; - } - - if (diffFileHeader) { - adjustment -= diffFileHeader.offsetHeight; - } - - if (versionMenusContainer) { - adjustment -= versionMenusContainer.offsetHeight; - } + adjustment -= getElementOffsetHeight(fixedNav); + adjustment -= getElementOffsetHeight(fixedTabs); + adjustment -= getElementOffsetHeight(fixedDiffStats); + adjustment -= getElementOffsetHeight(performanceBar); + adjustment -= getElementOffsetHeight(diffFileHeader); + adjustment -= getElementOffsetHeight(versionMenusContainer); if (isInIssuePage()) { - adjustment -= fixedIssuableTitle.offsetHeight; + adjustment -= getElementOffsetHeight(fixedIssuableTitle); } if (isInMRPage()) { adjustment -= topPadding; } + if (target?.scrollIntoView) { + target.scrollIntoView(true); + } + setTimeout(() => { window.scrollBy(0, adjustment); }); @@ -172,7 +167,7 @@ export const contentTop = () => { return size; }, - () => getOuterHeight('.merge-request-tabs'), + () => getOuterHeight('.merge-request-sticky-header, .merge-request-tabs'), () => getOuterHeight('.js-diff-files-changed'), () => getOuterHeight('.issue-sticky-header.gl-fixed'), ({ desktop }) => { @@ -180,7 +175,9 @@ export const contentTop = () => { let size; if (desktop && diffsTabIsActive) { - size = getOuterHeight('.diff-file .file-title-flex-parent:not([style="display:none"])'); + size = getOuterHeight( + '.diffs .diff-file .file-title-flex-parent:not([style="display:none"])', + ); } return size; diff --git a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js index 6c5d4ecc901..c11cf1a7882 100644 --- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js @@ -270,24 +270,6 @@ export const secondsToMilliseconds = (seconds) => seconds * 1000; */ export const secondsToDays = (seconds) => Math.round(seconds / 86400); -/** - * Converts a numeric utc offset in seconds to +/- hours - * ie -32400 => -9 hours - * ie -12600 => -3.5 hours - * - * @param {Number} offset UTC offset in seconds as a integer - * - * @return {String} the + or - offset in hours - */ -export const secondsToHours = (offset) => { - const parsed = parseInt(offset, 10); - if (Number.isNaN(parsed) || parsed === 0) { - return `0`; - } - const num = offset / 3600; - return parseInt(num, 10) !== num ? num.toFixed(1) : num; -}; - /** * Returns the date `n` days after the date provided * 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 d07abb72210..737c18d1bce 100644 --- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js +++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js @@ -406,3 +406,29 @@ export const durationTimeFormatted = (duration) => { return `${hh}:${mm}:${ss}`; }; + +/** + * Converts a numeric utc offset in seconds to +/- hours + * ie -32400 => -9 hours + * ie -12600 => -3.5 hours + * + * @param {Number} offset UTC offset in seconds as a integer + * + * @return {String} the + or - offset in hours, e.g. `- 10`, `0`, `+ 4` + */ +export const formatUtcOffset = (offset) => { + const parsed = parseInt(offset, 10); + if (Number.isNaN(parsed) || parsed === 0) { + return `0`; + } + const prefix = offset > 0 ? '+' : '-'; + return `${prefix} ${Math.abs(offset / 3600)}`; +}; + +/** + * Returns formatted timezone + * + * @param {Object} timezone item with offset and name + * @returns {String} the UTC timezone with the offset, e.g. `[UTC + 2] Berlin` + */ +export const formatTimezone = ({ offset, name }) => `[UTC ${formatUtcOffset(offset)}] ${name}`; diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 48be8af3ff6..3894ec36a0b 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -391,13 +391,15 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo /** * Indents selected lines to the right by 2 spaces * - * @param {Object} textArea - the targeted text area + * @param {Object} textArea - jQuery object with the targeted text area */ -function indentLines(textArea) { +function indentLines($textArea) { + const textArea = $textArea.get(0); const { lines, selectionStart, selectionEnd, startPos, endPos } = linesFromSelection(textArea); const shiftedLines = []; let totalAdded = 0; + textArea.focus(); textArea.setSelectionRange(startPos, endPos); lines.forEach((line) => { @@ -418,13 +420,15 @@ function indentLines(textArea) { * * @param {Object} textArea - the targeted text area */ -function outdentLines(textArea) { +function outdentLines($textArea) { + const textArea = $textArea.get(0); const { lines, selectionStart, selectionEnd, startPos, endPos } = linesFromSelection(textArea); const shiftedLines = []; let totalRemoved = 0; let removedFromFirstline = -1; let removedFromLine = 0; + textArea.focus(); textArea.setSelectionRange(startPos, endPos); lines.forEach((line) => { @@ -460,28 +464,10 @@ function outdentLines(textArea) { ); } -function handleIndentOutdent(e, textArea) { - if (e.altKey || e.ctrlKey || e.shiftKey) return; - if (!e.metaKey) return; - - switch (e.key) { - case ']': - e.preventDefault(); - indentLines(textArea); - break; - case '[': - e.preventDefault(); - outdentLines(textArea); - break; - default: - break; - } -} - /* eslint-disable @gitlab/require-i18n-strings */ function handleSurroundSelectedText(e, textArea) { if (!gon.markdown_surround_selection) return; - if (e.metaKey) return; + if (e.metaKey || e.ctrlKey) return; if (textArea.selectionStart === textArea.selectionEnd) return; const keys = { @@ -532,6 +518,7 @@ function continueOlText(listLineMatch, nextLineMatch) { } function handleContinueList(e, textArea) { + if (!gon.markdown_automatic_lists) return; if (!(e.key === 'Enter')) return; if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; if (textArea.selectionStart !== textArea.selectionEnd) return; @@ -586,7 +573,6 @@ export function keypressNoteText(e) { if ($(textArea).atwho?.('isSelecting')) return; - handleIndentOutdent(e, textArea); handleContinueList(e, textArea); handleSurroundSelectedText(e, textArea); } @@ -600,15 +586,26 @@ export function compositionEndNoteText() { } 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.attr('data-md-tag-content'), - }); + const $textArea = $toolbarBtn.closest('.md-area').find('textarea'); + + switch ($toolbarBtn.data('mdCommand')) { + case 'indentLines': + indentLines($textArea); + break; + case 'outdentLines': + outdentLines($textArea); + break; + default: + return updateText({ + textArea: $textArea, + tag: $toolbarBtn.data('mdTag'), + cursorOffset: $toolbarBtn.data('mdCursorOffset'), + blockTag: $toolbarBtn.data('mdBlock'), + wrap: !$toolbarBtn.data('mdPrepend'), + select: $toolbarBtn.data('mdSelect'), + tagContent: $toolbarBtn.attr('data-md-tag-content'), + }); + } } export function addMarkdownListeners(form) { diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 59645d50e29..367180714df 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,5 +1,5 @@ import { isString, memoize } from 'lodash'; - +import { base64ToBuffer, bufferToBase64 } from '~/authentication/webauthn/util'; import { TRUNCATE_WIDTH_DEFAULT_WIDTH, TRUNCATE_WIDTH_DEFAULT_FONT_SIZE, @@ -513,3 +513,15 @@ export const limitedCounterWithDelimiter = (count) => { return count > limit ? '1,000+' : count; }; + +// Encoding UTF8 ⇢ base64 +export function base64EncodeUnicode(str) { + const encoder = new TextEncoder('utf8'); + return bufferToBase64(encoder.encode(str)); +} + +// Decoding base64 ⇢ UTF8 +export function base64DecodeUnicode(str) { + const decoder = new TextDecoder('utf8'); + return decoder.decode(base64ToBuffer(str)); +} -- cgit v1.2.3