diff options
Diffstat (limited to 'app')
357 files changed, 5581 insertions, 5130 deletions
diff --git a/app/assets/images/koding-logo.svg b/app/assets/images/koding-logo.svg deleted file mode 100644 index ad89d684d94..00000000000 --- a/app/assets/images/koding-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14"> - <g fill="#d6d7d9"> - <path d="M8.7 0L5.3.3l3.2 6.8-3.2 6.6 3.5.3L12 6.9z"/> - <ellipse cx="1.7" cy="11.1" rx="1.7" ry="1.7"/> - <ellipse cx="1.7" cy="5.6" rx="1.7" ry="1.7"/> - </g> -</svg>
\ No newline at end of file diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index 00419e80cbb..9a33a060c76 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -51,7 +51,7 @@ export default function initCopyToClipboard() { * the last minute to deconstruct this JSON hash and set the `text/plain` and `text/x-gfm` copy * data types to the intended values. */ - $(document).on('copy', 'body > textarea[readonly]', (e) => { + $(document).on('copy', 'body > textarea[readonly]', e => { const { clipboardData } = e.originalEvent; if (!clipboardData) return; diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js index 1d63f5baeee..9bdfc21c7e4 100644 --- a/app/assets/javascripts/behaviors/details_behavior.js +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -2,7 +2,9 @@ import $ from 'jquery'; $(() => { $('body').on('click', '.js-details-target', function target() { - $(this).closest('.js-details-container').toggleClass('open'); + $(this) + .closest('.js-details-container') + .toggleClass('open'); }); // Show details content. Hides link after click. @@ -13,7 +15,9 @@ $(() => { // $('body').on('click', '.js-details-expand', function expand(e) { e.preventDefault(); - $(this).next('.js-details-content').removeClass('hide'); + $(this) + .next('.js-details-content') + .removeClass('hide'); $(this).hide(); const truncatedItem = $(this).siblings('.js-details-short'); diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 0d7e8a5a3cb..fe02096d903 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -34,7 +34,7 @@ const gfmRules = { }, }, AutolinkFilter: { - 'a'(el, text) { + a(el, text) { // Fallback on the regular MarkdownFilter's `a` handler. if (text !== el.getAttribute('href')) return false; @@ -60,7 +60,7 @@ const gfmRules = { }, }, ImageLazyLoadFilter: { - 'img'(el, text) { + img(el, text) { return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; }, }, @@ -71,7 +71,7 @@ const gfmRules = { return CopyAsGFM.nodeToGFM(videoEl); }, - 'video'(el) { + video(el) { return `![${el.dataset.title}](${el.getAttribute('src')})`; }, }, @@ -118,11 +118,14 @@ const gfmRules = { 'a[name]:not([href]):empty'(el) { return el.outerHTML; }, - 'dl'(el, text) { - let lines = text.replace(/\n\n/g, '\n').trim().split('\n'); + dl(el, text) { + let lines = text + .replace(/\n\n/g, '\n') + .trim() + .split('\n'); // Add two spaces to the front of subsequent list items lines, // or leave the line entirely blank. - lines = lines.map((l) => { + lines = lines.map(l => { const line = l.trim(); if (line.length === 0) return ''; @@ -151,27 +154,30 @@ const gfmRules = { // Prefixes lines with 4 spaces if the code contains triple backticks if (lang === '' && text.match(/^```/gm)) { - return text.split('\n').map((l) => { - const line = l.trim(); - if (line.length === 0) return ''; - - return ` ${line}`; - }).join('\n'); + return text + .split('\n') + .map(l => { + const line = l.trim(); + if (line.length === 0) return ''; + + return ` ${line}`; + }) + .join('\n'); } return `\`\`\`${lang}\n${text}\n\`\`\``; }, 'pre > code'(el, text) { - // Don't wrap code blocks in `` + // Don't wrap code blocks in `` return text; }, }, MarkdownFilter: { - 'br'(el) { + br(el) { // Two spaces at the end of a line are turned into a BR return ' '; }, - 'code'(el, text) { + code(el, text) { let backtickCount = 1; const backtickMatch = text.match(/`+/); if (backtickMatch) { @@ -183,27 +189,31 @@ const gfmRules = { return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks; }, - 'blockquote'(el, text) { - return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); + blockquote(el, text) { + return text + .trim() + .split('\n') + .map(s => `> ${s}`.trim()) + .join('\n'); }, - 'img'(el) { + img(el) { const imageSrc = el.src; - const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || ''); + const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || ''; return `![${el.getAttribute('alt')}](${imageUrl})`; }, 'a.anchor'(el, text) { // Don't render a Markdown link for the anchor link inside a heading return text; }, - 'a'(el, text) { + a(el, text) { return `[${text}](${el.getAttribute('href')})`; }, - 'li'(el, text) { + li(el, text) { const lines = text.trim().split('\n'); const firstLine = `- ${lines.shift()}`; // Add four spaces to the front of subsequent list items lines, // or leave the line entirely blank. - const nextLines = lines.map((s) => { + const nextLines = lines.map(s => { if (s.trim().length === 0) return ''; return ` ${s}`; @@ -211,49 +221,49 @@ const gfmRules = { return `${firstLine}\n${nextLines.join('\n')}`; }, - 'ul'(el, text) { + ul(el, text) { return text; }, - 'ol'(el, text) { + ol(el, text) { // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists. - return text.replace(/^- /mg, '1. '); + return text.replace(/^- /gm, '1. '); }, - 'h1'(el, text) { + h1(el, text) { return `# ${text.trim()}\n`; }, - 'h2'(el, text) { + h2(el, text) { return `## ${text.trim()}\n`; }, - 'h3'(el, text) { + h3(el, text) { return `### ${text.trim()}\n`; }, - 'h4'(el, text) { + h4(el, text) { return `#### ${text.trim()}\n`; }, - 'h5'(el, text) { + h5(el, text) { return `##### ${text.trim()}\n`; }, - 'h6'(el, text) { + h6(el, text) { return `###### ${text.trim()}\n`; }, - 'strong'(el, text) { + strong(el, text) { return `**${text}**`; }, - 'em'(el, text) { + em(el, text) { return `_${text}_`; }, - 'del'(el, text) { + del(el, text) { return `~~${text}~~`; }, - 'hr'(el) { + hr(el) { // extra leading \n is to ensure that there is a blank line between // a list followed by an hr, otherwise this breaks old redcarpet rendering return '\n-----\n'; }, - 'p'(el, text) { + p(el, text) { return `${text.trim()}\n`; }, - 'table'(el) { + table(el) { const theadEl = el.querySelector('thead'); const tbodyEl = el.querySelector('tbody'); if (!theadEl || !tbodyEl) return false; @@ -263,8 +273,8 @@ const gfmRules = { return [theadText, tbodyText].join('\n'); }, - 'thead'(el, text) { - const cells = _.map(el.querySelectorAll('th'), (cell) => { + thead(el, text) { + const cells = _.map(el.querySelectorAll('th'), cell => { let chars = CopyAsGFM.nodeToGFM(cell).length + 2; let before = ''; @@ -296,7 +306,7 @@ const gfmRules = { return [text, separatorRow].join('\n'); }, - 'tr'(el) { + tr(el) { const cellEls = el.querySelectorAll('td, th'); if (cellEls.length === 0) return false; @@ -315,8 +325,12 @@ export class CopyAsGFM { const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent); if (isIOS) return; - $(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); - $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); + $(document).on('copy', '.md, .wiki', e => { + CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); + }); + $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', e => { + CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); + }); $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM); } @@ -356,7 +370,7 @@ export class CopyAsGFM { // This will break down when the actual code block contains an uneven // number of backticks, but this is a rare edge case. const backtickMatch = textBefore.match(/`/g); - const insideCodeBlock = backtickMatch && (backtickMatch.length % 2) === 1; + const insideCodeBlock = backtickMatch && backtickMatch.length % 2 === 1; if (insideCodeBlock) { return text; @@ -393,7 +407,9 @@ export class CopyAsGFM { let lineSelector = '.line'; if (target) { - const lineClass = ['left-side', 'right-side'].filter(name => target.classList.contains(name))[0]; + const lineClass = ['left-side', 'right-side'].filter(name => + target.classList.contains(name), + )[0]; if (lineClass) { lineSelector = `.line_content.${lineClass} ${lineSelector}`; } @@ -436,7 +452,8 @@ export class CopyAsGFM { return node.textContent; } - const respectWhitespace = respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE'); + const respectWhitespace = + respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE'); const text = this.innerGFM(node, respectWhitespace); diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index eb4e59d12b1..a68936d79e2 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -32,7 +32,9 @@ export default function renderMath($els) { Promise.all([ import(/* webpackChunkName: 'katex' */ 'katex'), import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'), - ]).then(([katex]) => { - renderWithKaTeX($els, katex); - }).catch(() => flash(__('An error occurred while rendering KaTeX'))); + ]) + .then(([katex]) => { + renderWithKaTeX($els, katex); + }) + .catch(() => flash(__('An error occurred while rendering KaTeX'))); } diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js index 56b1896e9f1..56f64f934a1 100644 --- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js +++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js @@ -17,41 +17,43 @@ import flash from '~/flash'; export default function renderMermaid($els) { if (!$els.length) return; - import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid').then((mermaid) => { - mermaid.initialize({ - // mermaid core options - mermaid: { - startOnLoad: false, - }, - // mermaidAPI options - theme: 'neutral', - }); + import(/* webpackChunkName: 'mermaid' */ 'blackst0ne-mermaid') + .then(mermaid => { + mermaid.initialize({ + // mermaid core options + mermaid: { + startOnLoad: false, + }, + // mermaidAPI options + theme: 'neutral', + }); - $els.each((i, el) => { - const source = el.textContent; + $els.each((i, el) => { + const source = el.textContent; - // Remove any extra spans added by the backend syntax highlighting. - Object.assign(el, { textContent: source }); + // Remove any extra spans added by the backend syntax highlighting. + Object.assign(el, { textContent: source }); - mermaid.init(undefined, el, (id) => { - const svg = document.getElementById(id); + mermaid.init(undefined, el, id => { + const svg = document.getElementById(id); - svg.classList.add('mermaid'); + svg.classList.add('mermaid'); - // pre > code > svg - svg.closest('pre').replaceWith(svg); + // pre > code > svg + svg.closest('pre').replaceWith(svg); - // We need to add the original source into the DOM to allow Copy-as-GFM - // to access it. - const sourceEl = document.createElement('text'); - sourceEl.classList.add('source'); - sourceEl.setAttribute('display', 'none'); - sourceEl.textContent = source; + // We need to add the original source into the DOM to allow Copy-as-GFM + // to access it. + const sourceEl = document.createElement('text'); + sourceEl.classList.add('source'); + sourceEl.setAttribute('display', 'none'); + sourceEl.textContent = source; - svg.appendChild(sourceEl); + svg.appendChild(sourceEl); + }); }); + }) + .catch(err => { + flash(`Can't load mermaid module: ${err}`); }); - }).catch((err) => { - flash(`Can't load mermaid module: ${err}`); - }); } diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js index 0964baf8954..35f1bb6b080 100644 --- a/app/assets/javascripts/behaviors/preview_markdown.js +++ b/app/assets/javascripts/behaviors/preview_markdown.js @@ -26,7 +26,7 @@ MarkdownPreview.prototype.emptyMessage = 'Nothing to preview.'; MarkdownPreview.prototype.ajaxCache = {}; -MarkdownPreview.prototype.showPreview = function ($form) { +MarkdownPreview.prototype.showPreview = function($form) { var mdText; var markdownVersion; var url; @@ -44,34 +44,40 @@ MarkdownPreview.prototype.showPreview = function ($form) { this.hideReferencedUsers($form); } else { preview.addClass('md-preview-loading').text('Loading...'); - this.fetchMarkdownPreview(mdText, url, (function (response) { - var body; - if (response.body.length > 0) { - ({ body } = response); - } else { - body = this.emptyMessage; - } - - preview.removeClass('md-preview-loading').html(body); - preview.renderGFM(); - this.renderReferencedUsers(response.references.users, $form); - - if (response.references.commands) { - this.renderReferencedCommands(response.references.commands, $form); - } - }).bind(this)); + this.fetchMarkdownPreview( + mdText, + url, + function(response) { + var body; + if (response.body.length > 0) { + ({ body } = response); + } else { + body = this.emptyMessage; + } + + preview.removeClass('md-preview-loading').html(body); + preview.renderGFM(); + this.renderReferencedUsers(response.references.users, $form); + + if (response.references.commands) { + this.renderReferencedCommands(response.references.commands, $form); + } + }.bind(this), + ); } }; -MarkdownPreview.prototype.versionedPreviewPath = function (markdownPreviewPath, markdownVersion) { +MarkdownPreview.prototype.versionedPreviewPath = function(markdownPreviewPath, markdownVersion) { if (typeof markdownVersion === 'undefined') { return markdownPreviewPath; } - return `${markdownPreviewPath}${markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'}markdown_version=${markdownVersion}`; + return `${markdownPreviewPath}${ + markdownPreviewPath.indexOf('?') === -1 ? '?' : '&' + }markdown_version=${markdownVersion}`; }; -MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { +MarkdownPreview.prototype.fetchMarkdownPreview = function(text, url, success) { if (!url) { return; } @@ -79,24 +85,25 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { success(this.ajaxCache.response); return; } - axios.post(url, { - text, - }) - .then(({ data }) => { - this.ajaxCache = { - text: text, - response: data, - }; - success(data); - }) - .catch(() => flash(__('An error occurred while fetching markdown preview'))); + axios + .post(url, { + text, + }) + .then(({ data }) => { + this.ajaxCache = { + text: text, + response: data, + }; + success(data); + }) + .catch(() => flash(__('An error occurred while fetching markdown preview'))); }; -MarkdownPreview.prototype.hideReferencedUsers = function ($form) { +MarkdownPreview.prototype.hideReferencedUsers = function($form) { $form.find('.referenced-users').hide(); }; -MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { +MarkdownPreview.prototype.renderReferencedUsers = function(users, $form) { var referencedUsers; referencedUsers = $form.find('.referenced-users'); if (referencedUsers.length) { @@ -109,11 +116,11 @@ MarkdownPreview.prototype.renderReferencedUsers = function (users, $form) { } }; -MarkdownPreview.prototype.hideReferencedCommands = function ($form) { +MarkdownPreview.prototype.hideReferencedCommands = function($form) { $form.find('.referenced-commands').hide(); }; -MarkdownPreview.prototype.renderReferencedCommands = function (commands, $form) { +MarkdownPreview.prototype.renderReferencedCommands = function(commands, $form) { var referencedCommands; referencedCommands = $form.find('.referenced-commands'); if (commands.length > 0) { @@ -132,14 +139,14 @@ writeButtonSelector = '.js-md-write-button'; lastTextareaPreviewed = null; const markdownToolbar = $('.md-header-toolbar'); -$.fn.setupMarkdownPreview = function () { +$.fn.setupMarkdownPreview = function() { var $form = $(this); - $form.find('textarea.markdown-area').on('input', function () { + $form.find('textarea.markdown-area').on('input', function() { markdownPreview.hideReferencedUsers($form); }); }; -$(document).on('markdown-preview:show', function (e, $form) { +$(document).on('markdown-preview:show', function(e, $form) { if (!$form) { return; } @@ -148,8 +155,14 @@ $(document).on('markdown-preview:show', function (e, $form) { lastTextareaHeight = lastTextareaPreviewed.height(); // toggle tabs - $form.find(writeButtonSelector).parent().removeClass('active'); - $form.find(previewButtonSelector).parent().addClass('active'); + $form + .find(writeButtonSelector) + .parent() + .removeClass('active'); + $form + .find(previewButtonSelector) + .parent() + .addClass('active'); // toggle content $form.find('.md-write-holder').hide(); @@ -158,7 +171,7 @@ $(document).on('markdown-preview:show', function (e, $form) { markdownPreview.showPreview($form); }); -$(document).on('markdown-preview:hide', function (e, $form) { +$(document).on('markdown-preview:hide', function(e, $form) { if (!$form) { return; } @@ -169,8 +182,14 @@ $(document).on('markdown-preview:hide', function (e, $form) { } // toggle tabs - $form.find(writeButtonSelector).parent().addClass('active'); - $form.find(previewButtonSelector).parent().removeClass('active'); + $form + .find(writeButtonSelector) + .parent() + .addClass('active'); + $form + .find(previewButtonSelector) + .parent() + .removeClass('active'); // toggle content $form.find('.md-write-holder').show(); @@ -181,7 +200,7 @@ $(document).on('markdown-preview:hide', function (e, $form) { markdownPreview.hideReferencedCommands($form); }); -$(document).on('markdown-preview:toggle', function (e, keyboardEvent) { +$(document).on('markdown-preview:toggle', function(e, keyboardEvent) { var $target; $target = $(keyboardEvent.target); if ($target.is('textarea.markdown-area')) { @@ -194,14 +213,14 @@ $(document).on('markdown-preview:toggle', function (e, keyboardEvent) { } }); -$(document).on('click', previewButtonSelector, function (e) { +$(document).on('click', previewButtonSelector, function(e) { var $form; e.preventDefault(); $form = $(this).closest('form'); $(document).triggerHandler('markdown-preview:show', [$form]); }); -$(document).on('click', writeButtonSelector, function (e) { +$(document).on('click', writeButtonSelector, function(e) { var $form; e.preventDefault(); $form = $(this).closest('form'); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index b6e2781773c..c1ea67f9293 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -28,7 +28,7 @@ function keyCodeIs(e, keyCode) { return e.keyCode === keyCode; } -$(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { +$(document).on('keydown.quick_submit', '.js-quick-submit', e => { // Enter if (!keyCodeIs(e, 13)) { return; @@ -55,23 +55,25 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { // If the user tabs to a submit button on a `js-quick-submit` form, display a // tooltip to let them know they could've used the hotkey -$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function displayTooltip(e) { - // Tab - if (!keyCodeIs(e, 9)) { - return; - } +$(document).on( + 'keyup.quick_submit', + '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', + function displayTooltip(e) { + // Tab + if (!keyCodeIs(e, 9)) { + return; + } - const $this = $(this); - const title = isMac() ? - 'You can also press ⌘-Enter' : - 'You can also press Ctrl-Enter'; + const $this = $(this); + const title = isMac() ? 'You can also press ⌘-Enter' : 'You can also press Ctrl-Enter'; - $this.tooltip({ - container: 'body', - html: 'true', - placement: 'top', - title, - trigger: 'manual', - }); - $this.tooltip('show').one('blur click', () => $this.tooltip('hide')); -}); + $this.tooltip({ + container: 'body', + html: 'true', + placement: 'top', + title, + trigger: 'manual', + }); + $this.tooltip('show').one('blur click', () => $this.tooltip('hide')); + }, +); diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index a8b6dbf0948..c09d9ccddd6 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -18,7 +18,8 @@ import '../commons/bootstrap'; $.fn.requiresInput = function requiresInput() { const $form = $(this); const $button = $('button[type=submit], input[type=submit]', $form); - const fieldSelector = 'input[required=required], select[required=required], textarea[required=required]'; + const fieldSelector = + 'input[required=required], select[required=required], textarea[required=required]'; function requireInput() { // Collect the input values of *all* required fields diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js index 0d6e0dbefcc..f6bf62d734e 100644 --- a/app/assets/javascripts/behaviors/secret_values.js +++ b/app/assets/javascripts/behaviors/secret_values.js @@ -32,16 +32,18 @@ export default class SecretValues { updateDom(isRevealed) { const values = this.container.querySelectorAll(this.valueSelector); - values.forEach((value) => { + values.forEach(value => { value.classList.toggle('hide', !isRevealed); }); const placeholders = this.container.querySelectorAll(this.placeholderSelector); - placeholders.forEach((placeholder) => { + placeholders.forEach(placeholder => { placeholder.classList.toggle('hide', isRevealed); }); - this.revealButton.textContent = isRevealed ? n__('Hide value', 'Hide values', values.length) : n__('Reveal value', 'Reveal values', values.length); + this.revealButton.textContent = isRevealed + ? n__('Hide value', 'Hide values', values.length) + : n__('Reveal value', 'Reveal values', values.length); this.revealButton.dataset.secretRevealStatus = isRevealed; } } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 6719bfd6d22..8b5a3c1c69d 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -88,22 +88,24 @@ export default class Shortcuts { return null; } - return axios.get(gon.shortcuts_path, { - responseType: 'text', - }).then(({ data }) => { - $.globalEval(data); - - if (location && location.length > 0) { - const results = []; - for (let i = 0, len = location.length; i < len; i += 1) { - results.push($(location[i]).show()); + return axios + .get(gon.shortcuts_path, { + responseType: 'text', + }) + .then(({ data }) => { + $.globalEval(data); + + if (location && location.length > 0) { + const results = []; + for (let i = 0, len = location.length; i < len; i += 1) { + results.push($(location[i]).show()); + } + return results; } - return results; - } - $('.hidden-shortcut').show(); - return $('.js-more-help-button').remove(); - }); + $('.hidden-shortcut').show(); + return $('.js-more-help-button').remove(); + }); } focusFilter(e) { diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 4446be0e52f..ef8b8788abf 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -18,9 +18,7 @@ $(() => { .toggleClass('fa-chevron-up', toggleState) .toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined); - $container - .find('.js-toggle-content') - .toggle(toggleState); + $container.find('.js-toggle-content').toggle(toggleState); } $('body').on('click', '.js-toggle-button', function toggleButton(e) { diff --git a/app/assets/javascripts/blob/3d_viewer/index.js b/app/assets/javascripts/blob/3d_viewer/index.js index 1bdf1aeb76c..2d4f45cc365 100644 --- a/app/assets/javascripts/blob/3d_viewer/index.js +++ b/app/assets/javascripts/blob/3d_viewer/index.js @@ -18,12 +18,7 @@ export default class Renderer { this.loader = new STLLoader(); this.fov = 45; - this.camera = new THREE.PerspectiveCamera( - this.fov, - this.width / this.height, - 1, - 1000, - ); + this.camera = new THREE.PerspectiveCamera(this.fov, this.width / this.height, 1, 1000); this.scene = new THREE.Scene(); @@ -35,10 +30,7 @@ export default class Renderer { this.setupLight(); // Set up OrbitControls - this.controls = new OrbitControls( - this.camera, - this.renderer.domElement, - ); + this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls.minDistance = 5; this.controls.maxDistance = 30; this.controls.enableKeys = false; @@ -51,47 +43,32 @@ export default class Renderer { antialias: true, }); - this.renderer.setClearColor(0xFFFFFF); + this.renderer.setClearColor(0xffffff); this.renderer.setPixelRatio(window.devicePixelRatio); - this.renderer.setSize( - this.width, - this.height, - ); + this.renderer.setSize(this.width, this.height); } setupLight() { // Point light illuminates the object - const pointLight = new THREE.PointLight( - 0xFFFFFF, - 2, - 0, - ); + const pointLight = new THREE.PointLight(0xffffff, 2, 0); pointLight.castShadow = true; this.camera.add(pointLight); // Ambient light illuminates the scene - const ambientLight = new THREE.AmbientLight( - 0xFFFFFF, - 1, - ); + const ambientLight = new THREE.AmbientLight(0xffffff, 1); this.scene.add(ambientLight); } setupGrid() { - this.grid = new THREE.GridHelper( - 20, - 20, - 0x000000, - 0x000000, - ); + this.grid = new THREE.GridHelper(20, 20, 0x000000, 0x000000); this.scene.add(this.grid); } loadFile() { - this.loader.load(this.container.dataset.endpoint, (geo) => { + this.loader.load(this.container.dataset.endpoint, geo => { const obj = new MeshObject(geo); this.objects.push(obj); @@ -116,30 +93,23 @@ export default class Renderer { } render() { - this.renderer.render( - this.scene, - this.camera, - ); + this.renderer.render(this.scene, this.camera); requestAnimationFrame(this.renderWrapper); } changeObjectMaterials(type) { - this.objects.forEach((obj) => { + this.objects.forEach(obj => { obj.changeMaterial(type); }); } setDefaultCameraPosition() { const obj = this.objects[0]; - const radius = (obj.geometry.boundingSphere.radius / 1.5); - const dist = radius / (Math.sin((this.fov * (Math.PI / 180)) / 2)); - - this.camera.position.set( - 0, - dist + 1, - dist, - ); + const radius = obj.geometry.boundingSphere.radius / 1.5; + const dist = radius / Math.sin((this.fov * (Math.PI / 180)) / 2); + + this.camera.position.set(0, dist + 1, dist); this.camera.lookAt(this.grid); this.controls.update(); diff --git a/app/assets/javascripts/blob/3d_viewer/mesh_object.js b/app/assets/javascripts/blob/3d_viewer/mesh_object.js index 96758884abf..cb7fcff8674 100644 --- a/app/assets/javascripts/blob/3d_viewer/mesh_object.js +++ b/app/assets/javascripts/blob/3d_viewer/mesh_object.js @@ -1,10 +1,6 @@ -import { - Matrix4, - MeshLambertMaterial, - Mesh, -} from 'three/build/three.module'; +import { Matrix4, MeshLambertMaterial, Mesh } from 'three/build/three.module'; -const defaultColor = 0xE24329; +const defaultColor = 0xe24329; const materials = { default: new MeshLambertMaterial({ color: defaultColor, @@ -17,10 +13,7 @@ const materials = { export default class MeshObject extends Mesh { constructor(geo) { - super( - geo, - materials.default, - ); + super(geo, materials.default); this.geometry.computeBoundingSphere(); @@ -29,13 +22,7 @@ export default class MeshObject extends Mesh { if (this.geometry.boundingSphere.radius > 4) { const scale = 4 / this.geometry.boundingSphere.radius; - this.geometry.applyMatrix( - new Matrix4().makeScale( - scale, - scale, - scale, - ), - ); + this.geometry.applyMatrix(new Matrix4().makeScale(scale, scale, scale)); this.geometry.computeBoundingSphere(); this.position.x = -this.geometry.boundingSphere.center.x; diff --git a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js index 7986287f7e7..75777b910ca 100644 --- a/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js +++ b/app/assets/javascripts/blob/balsamiq/balsamiq_viewer.js @@ -42,7 +42,7 @@ class BalsamiqViewer { this.initDatabase(loadEvent.target.response); const previews = this.getPreviews(); - previews.forEach((preview) => { + previews.forEach(preview => { const renderedPreview = this.renderPreview(preview); container.appendChild(renderedPreview); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index ff1739b1679..cd3251ad1ca 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -41,39 +41,45 @@ export default class BlobFileDropzone { addRemoveLinks: true, previewsContainer: '.dropzone-previews', headers: csrf.headers, - init: function () { - this.on('addedfile', function () { + init: function() { + this.on('addedfile', function() { toggleLoading(submitButton, submitButtonLoadingIcon, false); dropzoneMessage.addClass(HIDDEN_CLASS); - $('.dropzone-alerts').html('').hide(); + $('.dropzone-alerts') + .html('') + .hide(); }); - this.on('removedfile', function () { + this.on('removedfile', function() { toggleLoading(submitButton, submitButtonLoadingIcon, false); dropzoneMessage.removeClass(HIDDEN_CLASS); }); - this.on('success', function (header, response) { + this.on('success', function(header, response) { $('#modal-upload-blob').modal('hide'); visitUrl(response.filePath); }); - this.on('maxfilesexceeded', function (file) { + this.on('maxfilesexceeded', function(file) { dropzoneMessage.addClass(HIDDEN_CLASS); this.removeFile(file); }); - this.on('sending', function (file, xhr, formData) { + this.on('sending', function(file, xhr, formData) { formData.append('branch_name', form.find('.js-branch-name').val()); formData.append('create_merge_request', form.find('.js-create-merge-request').val()); formData.append('commit_message', form.find('.js-commit-message').val()); }); }, // Override behavior of adding error underneath preview - error: function (file, errorMessage) { - const stripped = $('<div/>').html(errorMessage).text(); - $('.dropzone-alerts').html(`Error uploading file: "${stripped}"`).show(); + error: function(file, errorMessage) { + const stripped = $('<div/>') + .html(errorMessage) + .text(); + $('.dropzone-alerts') + .html(`Error uploading file: "${stripped}"`) + .show(); this.removeFile(file); }, }); - submitButton.on('click', (e) => { + submitButton.on('click', e => { e.preventDefault(); e.stopPropagation(); if (dropzone[0].dropzone.getQueuedFiles().length === 0) { diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js index d36d9f0de2d..62f0a56ed75 100644 --- a/app/assets/javascripts/blob/blob_line_permalink_updater.js +++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js @@ -2,17 +2,19 @@ import { getLocationHash } from '../lib/utils/url_utility'; const lineNumberRe = /^L[0-9]+/; -const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => { +const updateLineNumbersOnBlobPermalinks = linksToUpdate => { const hash = getLocationHash(); if (hash && lineNumberRe.test(hash)) { const hashUrlString = `#${hash}`; - [].concat(Array.prototype.slice.call(linksToUpdate)).forEach((permalinkButton) => { - const baseHref = permalinkButton.getAttribute('data-original-href') || (() => { - const href = permalinkButton.getAttribute('href'); - permalinkButton.setAttribute('data-original-href', href); - return href; - })(); + [].concat(Array.prototype.slice.call(linksToUpdate)).forEach(permalinkButton => { + const baseHref = + permalinkButton.getAttribute('data-original-href') || + (() => { + const href = permalinkButton.getAttribute('href'); + permalinkButton.setAttribute('data-original-href', href); + return href; + })(); permalinkButton.setAttribute('href', `${baseHref}${hashUrlString}`); }); } @@ -26,7 +28,7 @@ function BlobLinePermalinkUpdater(blobContentHolder, lineNumberSelector, element }, 0); }; - blobContentHolder.addEventListener('click', (e) => { + blobContentHolder.addEventListener('click', e => { if (e.target.matches(lineNumberSelector)) { updateBlameAndBlobPermalinkCb(); } diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js index 02228434a29..476901aae75 100644 --- a/app/assets/javascripts/blob/file_template_selector.js +++ b/app/assets/javascripts/blob/file_template_selector.js @@ -45,15 +45,11 @@ export default class FileTemplateSelector { } renderLoading() { - this.$loadingIcon - .addClass('fa-spinner fa-spin') - .removeClass('fa-chevron-down'); + this.$loadingIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down'); } renderLoaded() { - this.$loadingIcon - .addClass('fa-chevron-down') - .removeClass('fa-spinner fa-spin'); + this.$loadingIcon.addClass('fa-chevron-down').removeClass('fa-spinner fa-spin'); } reportSelection(options) { diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js index 6f1350e80fc..071022a9a75 100644 --- a/app/assets/javascripts/blob/notebook/index.js +++ b/app/assets/javascripts/blob/notebook/index.js @@ -40,13 +40,14 @@ export default () => { }, methods: { loadFile() { - axios.get(el.dataset.endpoint) + axios + .get(el.dataset.endpoint) .then(res => res.data) - .then((data) => { + .then(data => { this.json = data; this.loading = false; }) - .catch((e) => { + .catch(e => { if (e.status !== 200) { this.loadError = true; } diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js index 13318c58006..57c1baa9886 100644 --- a/app/assets/javascripts/blob/sketch/index.js +++ b/app/assets/javascripts/blob/sketch/index.js @@ -13,7 +13,7 @@ export default class SketchLoader { return this.getZipFile() .then(data => JSZip.loadAsync(data)) .then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array')) - .then((content) => { + .then(content => { const url = window.URL || window.webkitURL; const blob = new Blob([new Uint8Array(content)], { type: 'image/png', diff --git a/app/assets/javascripts/blob/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js index 339906adc34..f129b6e631e 100644 --- a/app/assets/javascripts/blob/stl_viewer.js +++ b/app/assets/javascripts/blob/stl_viewer.js @@ -3,8 +3,8 @@ import Renderer from './3d_viewer'; export default () => { const viewer = new Renderer(document.getElementById('js-stl-viewer')); - [].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => { - el.addEventListener('click', (e) => { + [].slice.call(document.querySelectorAll('.js-material-changer')).forEach(el => { + el.addEventListener('click', e => { const { target } = e; e.preventDefault(); diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 9db1fa70ffb..37e348d93d3 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -81,14 +81,10 @@ export default class TemplateSelector { } startLoadingSpinner() { - this.$dropdownIcon - .addClass('fa-spinner fa-spin') - .removeClass('fa-chevron-down'); + this.$dropdownIcon.addClass('fa-spinner fa-spin').removeClass('fa-chevron-down'); } stopLoadingSpinner() { - this.$dropdownIcon - .addClass('fa-chevron-down') - .removeClass('fa-spinner fa-spin'); + this.$dropdownIcon.addClass('fa-chevron-down').removeClass('fa-spinner fa-spin'); } } diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js index ac1fe95eee5..d01ab9257d6 100644 --- a/app/assets/javascripts/blob/template_selectors/license_selector.js +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -22,7 +22,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector { search: { fields: ['name'], }, - clicked: (options) => { + clicked: options => { const { e } = options; const el = options.$el; const query = options.selectedObj; diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js index a09381014a7..db3c144cbe3 100644 --- a/app/assets/javascripts/blob/template_selectors/type_selector.js +++ b/app/assets/javascripts/blob/template_selectors/type_selector.js @@ -21,5 +21,4 @@ export default class FileTemplateTypeSelector extends FileTemplateSelector { text: item => item.name, }); } - } diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 5485248cfaf..befa1dc455f 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -22,9 +22,8 @@ export default class BlobViewer { const viewer = document.querySelector('.blob-viewer[data-type="rich"]'); if (!viewer || !viewer.dataset.richType) return; - const initViewer = promise => promise - .then(module => module.default(viewer)) - .catch((error) => { + const initViewer = promise => + promise.then(module => module.default(viewer)).catch(error => { Flash('Error loading file viewer.'); throw error; }); @@ -79,10 +78,9 @@ export default class BlobViewer { initBindings() { if (this.switcherBtns.length) { - Array.from(this.switcherBtns) - .forEach((el) => { - el.addEventListener('click', this.switchViewHandler.bind(this)); - }); + Array.from(this.switcherBtns).forEach(el => { + el.addEventListener('click', this.switchViewHandler.bind(this)); + }); } if (this.copySourceBtn) { @@ -109,7 +107,10 @@ export default class BlobViewer { this.copySourceBtn.setAttribute('title', 'Copy source to clipboard'); this.copySourceBtn.classList.remove('disabled'); } else if (this.activeViewer === this.simpleViewer) { - this.copySourceBtn.setAttribute('title', 'Wait for the source to load to copy it to the clipboard'); + this.copySourceBtn.setAttribute( + 'title', + 'Wait for the source to load to copy it to the clipboard', + ); this.copySourceBtn.classList.add('disabled'); } else { this.copySourceBtn.setAttribute('title', 'Switch to the source to copy it to the clipboard'); @@ -147,15 +148,15 @@ export default class BlobViewer { this.toggleCopyButtonState(); BlobViewer.loadViewer(newViewer) - .then((viewer) => { - $(viewer).renderGFM(); + .then(viewer => { + $(viewer).renderGFM(); - this.$fileHolder.trigger('highlight:line'); - handleLocationHash(); + this.$fileHolder.trigger('highlight:line'); + handleLocationHash(); - this.toggleCopyButtonState(); - }) - .catch(() => new Flash('Error loading viewer')); + this.toggleCopyButtonState(); + }) + .catch(() => new Flash('Error loading viewer')); } static loadViewer(viewerParam) { @@ -168,12 +169,11 @@ export default class BlobViewer { viewer.setAttribute('data-loading', 'true'); - return axios.get(url) - .then(({ data }) => { - viewer.innerHTML = data.html; - viewer.setAttribute('data-loaded', 'true'); + return axios.get(url).then(({ data }) => { + viewer.innerHTML = data.html; + viewer.setAttribute('data-loaded', 'true'); - return viewer; - }); + return viewer; + }); } } diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index 4e4598870fa..3cc89ff1955 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -13,11 +13,11 @@ export default () => { if (editBlobForm.length) { const urlRoot = editBlobForm.data('relativeUrlRoot'); const assetsPath = editBlobForm.data('assetsPrefix'); - const blobLanguage = editBlobForm.data('blobLanguage'); + const filePath = editBlobForm.data('blobFilename') const currentAction = $('.js-file-title').data('currentAction'); const projectId = editBlobForm.data('project-id'); - new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction, projectId); + new EditBlob(`${urlRoot}${assetsPath}`, filePath, currentAction, projectId); new NewCommitForm(editBlobForm); } diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index ec2b130ab7d..6e19548eed2 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -5,6 +5,7 @@ import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; import { __ } from '~/locale'; import TemplateSelectorMediator from '../blob/file_template_mediator'; +import getModeByFileExtension from '~/lib/utils/ace_utils'; export default class EditBlob { constructor(assetsPath, aceMode, currentAction, projectId) { @@ -14,9 +15,10 @@ export default class EditBlob { this.initFileSelectors(currentAction, projectId); } - configureAceEditor(aceMode, assetsPath) { + configureAceEditor(filePath, assetsPath) { ace.config.set('modePath', `${assetsPath}/ace`); ace.config.loadModule('ace/ext/searchbox'); + ace.config.loadModule('ace/ext/modelist'); this.editor = ace.edit('editor'); @@ -25,8 +27,8 @@ export default class EditBlob { this.editor.focus(); - if (aceMode) { - this.editor.getSession().setMode(`ace/mode/${aceMode}`); + if (filePath) { + this.editor.getSession().setMode(getModeByFileExtension(filePath)); } } diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 28956c2f3c5..aa98f35786e 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; + import Icon from '~/vue_shared/components/icon.vue'; import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import eventHub from '../eventhub'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -8,6 +9,7 @@ export default { components: { UserAvatarLink, + Icon, }, directives: { tooltip, @@ -140,11 +142,11 @@ <div> <div class="board-card-header"> <h4 class="board-card-title"> - <i + <icon v-if="issue.confidential" - class="fa fa-eye-slash confidential-icon" - aria-hidden="true" - ></i> + name="eye-slash" + class="confidential-icon" + /> <a :href="issue.path" :title="issue.title" diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue index a58b5afe970..c93fd9f415c 100644 --- a/app/assets/javascripts/boards/components/modal/list.vue +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -1,4 +1,5 @@ <script> + import Icon from '~/vue_shared/components/icon.vue'; import bp from '../../../breakpoints'; import ModalStore from '../../stores/modal_store'; import IssueCardInner from '../issue_card_inner.vue'; @@ -6,6 +7,7 @@ export default { components: { IssueCardInner, + Icon, }, props: { issueLinkBase: { @@ -147,13 +149,13 @@ :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath"/> - <span + <icon v-if="issue.selected" :aria-label="'Issue #' + issue.id + ' selected'" + name="mobile-issue-close" aria-checked="true" - class="issue-card-selected text-center"> - <i class="fa fa-check"></i> - </span> + class="issue-card-selected text-center" + /> </div> </div> </div> diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 4622fd28220..3baac08d411 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -1,11 +1,13 @@ <script> import { Link } from '@gitlab-org/gitlab-ui'; +import Icon from '~/vue_shared/components/icon.vue'; import ModalStore from '../../stores/modal_store'; import boardsStore from '../../stores/boards_store'; export default { components: { 'gl-link': Link, + Icon, }, data() { return { @@ -35,7 +37,9 @@ export default { class="dropdown-label-box"> </span> {{ selected.title }} - <i class="fa fa-chevron-down"></i> + <icon + name="chevron-down" + /> </button> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> <ul> diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index d4676914e02..4e8fe16160a 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -1,11 +1,15 @@ <script> import $ from 'jquery'; import _ from 'underscore'; +import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../eventhub'; import Api from '../../api'; export default { name: 'BoardProjectSelect', + components: { + Icon, + }, props: { groupId: { type: Number, @@ -78,11 +82,9 @@ export default { aria-expanded="false" > {{ selectedProjectName }} - <i - class="fa fa-chevron-down" - aria-hidden="true" - > - </i> + <icon + name="chevron-down" + /> </button> <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width"> <div class="dropdown-title"> @@ -92,12 +94,11 @@ export default { type="button" class="dropdown-title-button dropdown-menu-close" > - <i - aria-hidden="true" + <icon + name="merge-request-close-m" data-hidden="true" - class="fa fa-times dropdown-menu-close-icon" - > - </i> + class="dropdown-menu-close-icon" + /> </button> </div> <div class="dropdown-input"> @@ -106,12 +107,11 @@ export default { type="search" placeholder="Search projects" /> - <i - aria-hidden="true" + <icon + name="search" + class="dropdown-input-search" data-hidden="true" - class="fa fa-search dropdown-input-search" - > - </i> + /> </div> <div class="dropdown-content"></div> <div class="dropdown-loading"> diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 65e7cee7039..ebf76af5966 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -1,6 +1,6 @@ import Visibility from 'visibilityjs'; import Vue from 'vue'; -import PersistentUserCallout from '../persistent_user_callout'; +import initDismissableCallout from '~/dismissable_callout'; import { s__, sprintf } from '../locale'; import Flash from '../flash'; import Poll from '../lib/utils/poll'; @@ -62,7 +62,7 @@ export default class Clusters { this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token'); - Clusters.initDismissableCallout(); + initDismissableCallout('.js-cluster-security-warning'); initSettingsPanels(); setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); this.initApplications(); @@ -105,12 +105,6 @@ export default class Clusters { }); } - static initDismissableCallout() { - const callout = document.querySelector('.js-cluster-security-warning'); - - if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new - } - addListeners() { if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js index e32d507d1f7..789c8360124 100644 --- a/app/assets/javascripts/clusters/clusters_index.js +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -1,15 +1,14 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; import setupToggleButtons from '~/toggle_buttons'; -import PersistentUserCallout from '../persistent_user_callout'; +import initDismissableCallout from '~/dismissable_callout'; import ClustersService from './services/clusters_service'; export default () => { const clusterList = document.querySelector('.js-clusters-list'); - const callout = document.querySelector('.gcp-signup-offer'); - if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new + initDismissableCallout('.gcp-signup-offer'); // The empty state won't have a clusterList if (clusterList) { diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 0452729d3ff..236bb1394c8 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -1,153 +1,162 @@ <script> - /* eslint-disable vue/require-default-prop */ - import { s__, sprintf } from '../../locale'; - import eventHub from '../event_hub'; - import identicon from '../../vue_shared/components/identicon.vue'; - import loadingButton from '../../vue_shared/components/loading_button.vue'; - import { - APPLICATION_STATUS, - REQUEST_LOADING, - REQUEST_SUCCESS, - REQUEST_FAILURE, - } from '../constants'; +/* eslint-disable vue/require-default-prop */ +import { s__, sprintf } from '../../locale'; +import eventHub from '../event_hub'; +import identicon from '../../vue_shared/components/identicon.vue'; +import loadingButton from '../../vue_shared/components/loading_button.vue'; +import { + APPLICATION_STATUS, + REQUEST_LOADING, + REQUEST_SUCCESS, + REQUEST_FAILURE, +} from '../constants'; - export default { - components: { - loadingButton, - identicon, +export default { + components: { + loadingButton, + identicon, + }, + props: { + id: { + type: String, + required: true, }, - props: { - id: { - type: String, - required: true, - }, - title: { - type: String, - required: true, - }, - titleLink: { - type: String, - required: false, - }, - manageLink: { - type: String, - required: false, - }, - logoUrl: { - type: String, - required: false, - default: null, - }, - disabled: { - type: Boolean, - required: false, - default: false, - }, - status: { - type: String, - required: false, - }, - statusReason: { - type: String, - required: false, - }, - requestStatus: { - type: String, - required: false, - }, - requestReason: { - type: String, - required: false, - }, - installApplicationRequestParams: { - type: Object, - required: false, - default: () => ({}), - }, + title: { + type: String, + required: true, }, - computed: { - isUnknownStatus() { - return !this.isKnownStatus && this.status !== null; - }, - isKnownStatus() { - return Object.values(APPLICATION_STATUS).includes(this.status); - }, - isInstalled() { - return ( - this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.UPDATED - ); - }, - hasLogo() { - return !!this.logoUrl; - }, - identiconId() { - // generate a deterministic integer id for the identicon background - return this.id.charCodeAt(0); - }, - rowJsClass() { - return `js-cluster-application-row-${this.id}`; - }, - installButtonLoading() { - return !this.status || - this.status === APPLICATION_STATUS.SCHEDULED || - this.status === APPLICATION_STATUS.INSTALLING || - this.requestStatus === REQUEST_LOADING; - }, - installButtonDisabled() { - // Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but - // we already made a request to install and are just waiting for the real-time - // to sync up. - return ((this.status !== APPLICATION_STATUS.INSTALLABLE - && this.status !== APPLICATION_STATUS.ERROR) || + titleLink: { + type: String, + required: false, + }, + manageLink: { + type: String, + required: false, + }, + logoUrl: { + type: String, + required: false, + default: null, + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + status: { + type: String, + required: false, + }, + statusReason: { + type: String, + required: false, + }, + requestStatus: { + type: String, + required: false, + }, + requestReason: { + type: String, + required: false, + }, + installApplicationRequestParams: { + type: Object, + required: false, + default: () => ({}), + }, + }, + computed: { + isUnknownStatus() { + return !this.isKnownStatus && this.status !== null; + }, + isKnownStatus() { + return Object.values(APPLICATION_STATUS).includes(this.status); + }, + isInstalled() { + return ( + this.status === APPLICATION_STATUS.INSTALLED || + this.status === APPLICATION_STATUS.UPDATED || + this.status === APPLICATION_STATUS.UPDATING + ); + }, + hasLogo() { + return !!this.logoUrl; + }, + identiconId() { + // generate a deterministic integer id for the identicon background + return this.id.charCodeAt(0); + }, + rowJsClass() { + return `js-cluster-application-row-${this.id}`; + }, + installButtonLoading() { + return ( + !this.status || + this.status === APPLICATION_STATUS.SCHEDULED || + this.status === APPLICATION_STATUS.INSTALLING || + this.requestStatus === REQUEST_LOADING + ); + }, + installButtonDisabled() { + // Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but + // we already made a request to install and are just waiting for the real-time + // to sync up. + return ( + ((this.status !== APPLICATION_STATUS.INSTALLABLE && + this.status !== APPLICATION_STATUS.ERROR) || this.requestStatus === REQUEST_LOADING || - this.requestStatus === REQUEST_SUCCESS) && this.isKnownStatus; - }, - installButtonLabel() { - let label; - if ( - this.status === APPLICATION_STATUS.NOT_INSTALLABLE || - this.status === APPLICATION_STATUS.INSTALLABLE || - this.status === APPLICATION_STATUS.ERROR || - this.isUnknownStatus - ) { - label = s__('ClusterIntegration|Install'); - } else if (this.status === APPLICATION_STATUS.SCHEDULED || - this.status === APPLICATION_STATUS.INSTALLING) { - label = s__('ClusterIntegration|Installing'); - } else if (this.status === APPLICATION_STATUS.INSTALLED || - this.status === APPLICATION_STATUS.UPDATED) { - label = s__('ClusterIntegration|Installed'); - } + this.requestStatus === REQUEST_SUCCESS) && + this.isKnownStatus + ); + }, + installButtonLabel() { + let label; + if ( + this.status === APPLICATION_STATUS.NOT_INSTALLABLE || + this.status === APPLICATION_STATUS.INSTALLABLE || + this.status === APPLICATION_STATUS.ERROR || + this.isUnknownStatus + ) { + label = s__('ClusterIntegration|Install'); + } else if ( + this.status === APPLICATION_STATUS.SCHEDULED || + this.status === APPLICATION_STATUS.INSTALLING + ) { + label = s__('ClusterIntegration|Installing'); + } else if ( + this.status === APPLICATION_STATUS.INSTALLED || + this.status === APPLICATION_STATUS.UPDATED || + this.status === APPLICATION_STATUS.UPDATING + ) { + label = s__('ClusterIntegration|Installed'); + } - return label; - }, - showManageButton() { - return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED; - }, - manageButtonLabel() { - return s__('ClusterIntegration|Manage'); - }, - hasError() { - return this.status === APPLICATION_STATUS.ERROR || - this.requestStatus === REQUEST_FAILURE; - }, - generalErrorDescription() { - return sprintf( - s__('ClusterIntegration|Something went wrong while installing %{title}'), { - title: this.title, - }, - ); - }, + return label; + }, + showManageButton() { + return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED; + }, + manageButtonLabel() { + return s__('ClusterIntegration|Manage'); + }, + hasError() { + return this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE; + }, + generalErrorDescription() { + return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), { + title: this.title, + }); }, - methods: { - installClicked() { - eventHub.$emit('installApplication', { - id: this.id, - params: this.installApplicationRequestParams, - }); - }, + }, + methods: { + installClicked() { + eventHub.$emit('installApplication', { + id: this.id, + params: this.installApplicationRequestParams, + }); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 72fc9355d82..24a49624583 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -6,6 +6,7 @@ export const APPLICATION_STATUS = { INSTALLING: 'installing', INSTALLED: 'installed', UPDATED: 'updated', + UPDATING: 'updating', ERROR: 'errored', }; diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue index 410d4873e55..82b0f523d2e 100644 --- a/app/assets/javascripts/cycle_analytics/components/banner.vue +++ b/app/assets/javascripts/cycle_analytics/components/banner.vue @@ -1,24 +1,28 @@ <script> - import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg'; +import Icon from '~/vue_shared/components/icon.vue'; +import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg'; - export default { - props: { - documentationLink: { - type: String, - required: true, - }, +export default { + components: { + Icon, + }, + props: { + documentationLink: { + type: String, + required: true, }, - computed: { - iconCycleAnalyticsSplash() { - return iconCycleAnalyticsSplash; - }, + }, + computed: { + iconCycleAnalyticsSplash() { + return iconCycleAnalyticsSplash; }, - methods: { - dismissOverviewDialog() { - this.$emit('dismiss-overview-dialog'); - }, + }, + methods: { + dismissOverviewDialog() { + this.$emit('dismiss-overview-dialog'); }, - }; + }, +}; </script> <template> <div class="landing content-block"> @@ -28,10 +32,9 @@ type="button" @click="dismissOverviewDialog" > - <i - class="fa fa-times" - aria-hidden="true"> - </i> + <icon + name="close" + /> </button> <div class="svg-container" diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue index b626b187651..f6a7d9962eb 100644 --- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue @@ -1,17 +1,17 @@ <script> - import tooltip from '../../vue_shared/directives/tooltip'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + props: { + count: { + type: Number, + required: true, }, - props: { - count: { - type: Number, - required: true, - }, - }, - }; + }, +}; </script> <template> <span diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue index a71dcf78103..429fef176c3 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.vue @@ -1,25 +1,25 @@ <script> - import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; - import limitWarning from './limit_warning_component.vue'; - import totalTime from './total_time_component.vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; +import limitWarning from './limit_warning_component.vue'; +import totalTime from './total_time_component.vue'; - export default { - components: { - userAvatarImage, - limitWarning, - totalTime, +export default { + components: { + userAvatarImage, + limitWarning, + totalTime, + }, + props: { + items: { + type: Array, + default: () => [], }, - props: { - items: { - type: Array, - default: () => [], - }, - stage: { - type: Object, - default: () => ({}), - }, + stage: { + type: Object, + default: () => ({}), }, - }; + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue index 312fe75dde4..56e851fa528 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_component.vue @@ -1,25 +1,25 @@ <script> - import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; - import limitWarning from './limit_warning_component.vue'; - import totalTime from './total_time_component.vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; +import limitWarning from './limit_warning_component.vue'; +import totalTime from './total_time_component.vue'; - export default { - components: { - userAvatarImage, - limitWarning, - totalTime, +export default { + components: { + userAvatarImage, + limitWarning, + totalTime, + }, + props: { + items: { + type: Array, + default: () => [], }, - props: { - items: { - type: Array, - default: () => [], - }, - stage: { - type: Object, - default: () => ({}), - }, + stage: { + type: Object, + default: () => ({}), }, - }; + }, +}; </script> <template> <div> @@ -73,4 +73,3 @@ </ul> </div> </template> - diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue index cee294b4ac2..54b9da4983a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue @@ -1,31 +1,31 @@ <script> - import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; - import iconCommit from '../svg/icon_commit.svg'; - import limitWarning from './limit_warning_component.vue'; - import totalTime from './total_time_component.vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; +import iconCommit from '../svg/icon_commit.svg'; +import limitWarning from './limit_warning_component.vue'; +import totalTime from './total_time_component.vue'; - export default { - components: { - userAvatarImage, - totalTime, - limitWarning, +export default { + components: { + userAvatarImage, + totalTime, + limitWarning, + }, + props: { + items: { + type: Array, + default: () => [], }, - props: { - items: { - type: Array, - default: () => [], - }, - stage: { - type: Object, - default: () => ({}), - }, + stage: { + type: Object, + default: () => ({}), }, - computed: { - iconCommit() { - return iconCommit; - }, + }, + computed: { + iconCommit() { + return iconCommit; }, - }; + }, +}; </script> <template> <div> @@ -74,4 +74,3 @@ </ul> </div> </template> - diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue index d4735d030fc..f9c80d237d7 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue @@ -1,27 +1,27 @@ <script> - import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; - import limitWarning from './limit_warning_component.vue'; - import totalTime from './total_time_component.vue'; - import icon from '../../vue_shared/components/icon.vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; +import limitWarning from './limit_warning_component.vue'; +import totalTime from './total_time_component.vue'; +import icon from '../../vue_shared/components/icon.vue'; - export default { - components: { - userAvatarImage, - totalTime, - limitWarning, - icon, +export default { + components: { + userAvatarImage, + totalTime, + limitWarning, + icon, + }, + props: { + items: { + type: Array, + default: () => [], }, - props: { - items: { - type: Array, - default: () => [], - }, - stage: { - type: Object, - default: () => ({}), - }, + stage: { + type: Object, + default: () => ({}), }, - }; + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue index 22637485c01..e83b66eef86 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue @@ -1,33 +1,33 @@ <script> - import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; - import iconBranch from '../svg/icon_branch.svg'; - import limitWarning from './limit_warning_component.vue'; - import totalTime from './total_time_component.vue'; - import icon from '../../vue_shared/components/icon.vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; +import iconBranch from '../svg/icon_branch.svg'; +import limitWarning from './limit_warning_component.vue'; +import totalTime from './total_time_component.vue'; +import icon from '../../vue_shared/components/icon.vue'; - export default { - components: { - userAvatarImage, - totalTime, - limitWarning, - icon, +export default { + components: { + userAvatarImage, + totalTime, + limitWarning, + icon, + }, + props: { + items: { + type: Array, + default: () => [], }, - props: { - items: { - type: Array, - default: () => [], - }, - stage: { - type: Object, - default: () => ({}), - }, + stage: { + type: Object, + default: () => ({}), }, - computed: { - iconBranch() { - return iconBranch; - }, + }, + computed: { + iconBranch() { + return iconBranch; }, - }; + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue index a0796f299e7..a8196dc879a 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue @@ -1,35 +1,35 @@ <script> - import iconBuildStatus from '../svg/icon_build_status.svg'; - import iconBranch from '../svg/icon_branch.svg'; - import limitWarning from './limit_warning_component.vue'; - import totalTime from './total_time_component.vue'; - import icon from '../../vue_shared/components/icon.vue'; +import iconBuildStatus from '../svg/icon_build_status.svg'; +import iconBranch from '../svg/icon_branch.svg'; +import limitWarning from './limit_warning_component.vue'; +import totalTime from './total_time_component.vue'; +import icon from '../../vue_shared/components/icon.vue'; - export default { - components: { - totalTime, - limitWarning, - icon, +export default { + components: { + totalTime, + limitWarning, + icon, + }, + props: { + items: { + type: Array, + default: () => [], }, - props: { - items: { - type: Array, - default: () => [], - }, - stage: { - type: Object, - default: () => ({}), - }, + stage: { + type: Object, + default: () => ({}), }, - computed: { - iconBuildStatus() { - return iconBuildStatus; - }, - iconBranch() { - return iconBranch; - }, + }, + computed: { + iconBuildStatus() { + return iconBuildStatus; }, - }; + iconBranch() { + return iconBranch; + }, + }, +}; </script> <template> <div> diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue index 7758bf0cb3f..4db50134208 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.vue @@ -1,18 +1,18 @@ <script> - export default { - props: { - time: { - type: Object, - required: false, - default: () => ({}), - }, +export default { + props: { + time: { + type: Object, + required: false, + default: () => ({}), }, - computed: { - hasData() { - return Object.keys(this.time).length; - }, + }, + computed: { + hasData() { + return Object.keys(this.time).length; }, - }; + }, +}; </script> <template> <span class="total-time"> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 1c43fc3cdc7..4de425b48e7 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -18,7 +18,8 @@ Vue.use(Translate); export default () => { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; - new Vue({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new Vue({ el: '#cycle-analytics', name: 'CycleAnalytics', components: { @@ -66,14 +67,17 @@ export default () => { const $dropdown = $('.js-ca-dropdown'); const $label = $dropdown.find('.dropdown-label'); - $dropdown.find('li a').off('click').on('click', (e) => { - e.preventDefault(); - const $target = $(e.currentTarget); - this.startDate = $target.data('value'); + $dropdown + .find('li a') + .off('click') + .on('click', e => { + e.preventDefault(); + const $target = $(e.currentTarget); + this.startDate = $target.data('value'); - $label.text($target.text().trim()); - this.fetchCycleAnalyticsData({ startDate: this.startDate }); - }); + $label.text($target.text().trim()); + this.fetchCycleAnalyticsData({ startDate: this.startDate }); + }); }, fetchCycleAnalyticsData(options) { const fetchOptions = options || { startDate: this.startDate }; @@ -82,7 +86,7 @@ export default () => { this.service .fetchCycleAnalyticsData(fetchOptions) - .then((response) => { + .then(response => { this.store.setCycleAnalyticsData(response); this.selectDefaultStage(); this.initDropdown(); @@ -115,7 +119,7 @@ export default () => { stage, startDate: this.startDate, }) - .then((response) => { + .then(response => { this.isEmptyStage = !response.events.length; this.store.setStageEvents(response.events, stage); this.isLoadingStage = false; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js index 4cf416c50e5..a0426301a0a 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js @@ -18,10 +18,7 @@ export default class CycleAnalyticsService { } fetchStageData(options) { - const { - stage, - startDate, - } = options; + const { stage, startDate } = options; return this.axios .get(`events/${stage.name}.json`, { diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index a8cd8c20f8f..18fb57c8b4f 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -5,13 +5,27 @@ import { dasherize } from '../lib/utils/text_utility'; import DEFAULT_EVENT_OBJECTS from './default_event_objects'; const EMPTY_STAGE_TEXTS = { - issue: __('The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.'), - plan: __('The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.'), - code: __('The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.'), - test: __('The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.'), - review: __('The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.'), - staging: __('The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.'), - production: __('The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.'), + issue: __( + 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.', + ), + plan: __( + 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.', + ), + code: __( + 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.', + ), + test: __( + 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.', + ), + review: __( + 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.', + ), + staging: __( + 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.', + ), + production: __( + 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.', + ), }; export default { @@ -31,11 +45,11 @@ export default { newData.stages = data.stats || []; newData.summary = data.summary || []; - newData.summary.forEach((item) => { + newData.summary.forEach(item => { item.value = item.value || '-'; }); - newData.stages.forEach((item) => { + newData.stages.forEach(item => { const stageSlug = dasherize(item.name.toLowerCase()); item.active = false; item.isUserAllowed = data.permissions[stageSlug]; @@ -53,7 +67,7 @@ export default { this.state.hasError = state; }, deactivateAllStages() { - this.state.stages.forEach((stage) => { + this.state.stages.forEach(stage => { stage.active = false; }); }, @@ -67,7 +81,7 @@ export default { decorateEvents(events, stage) { const newEvents = []; - events.forEach((item) => { + events.forEach(item => { if (!item) return; const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item); diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js index 87621761500..4ae4ceabc21 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js @@ -18,52 +18,56 @@ const CommentAndResolveBtn = Vue.extend({ }; }, computed: { - showButton: function () { + showButton: function() { if (this.discussion) { return this.discussion.isResolvable(); } else { return false; } }, - isDiscussionResolved: function () { + isDiscussionResolved: function() { return this.discussion.isResolved(); }, - buttonText: function () { + buttonText: function() { if (this.isDiscussionResolved) { if (this.textareaIsEmpty) { - return "Unresolve discussion"; + return 'Unresolve discussion'; } else { - return "Comment & unresolve discussion"; + return 'Comment & unresolve discussion'; } } else { if (this.textareaIsEmpty) { - return "Resolve discussion"; + return 'Resolve discussion'; } else { - return "Comment & resolve discussion"; + return 'Comment & resolve discussion'; } } - } + }, }, created() { if (this.discussionId) { this.discussion = CommentsStore.state[this.discussionId]; } }, - mounted: function () { + mounted: function() { if (!this.discussionId) return; - const $textarea = $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`); + const $textarea = $( + `.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`, + ); this.textareaIsEmpty = $textarea.val() === ''; $textarea.on('input.comment-and-resolve-btn', () => { this.textareaIsEmpty = $textarea.val() === ''; }); }, - destroyed: function () { + destroyed: function() { if (!this.discussionId) return; - $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off('input.comment-and-resolve-btn'); - } + $(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off( + 'input.comment-and-resolve-btn', + ); + }, }); Vue.component('comment-and-resolve-btn', CommentAndResolveBtn); diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index 5528d2a542b..5bdeaaade68 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -83,7 +83,11 @@ const DiffNoteAvatars = Vue.extend({ this.addNoCommentClass(); this.setDiscussionVisible(); - this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new'; + this.lineType = $(this.$el) + .closest('.diff-line-num') + .hasClass('old_line') + ? 'old' + : 'new'; }); $(document).on('toggle.comments', () => { @@ -113,20 +117,30 @@ const DiffNoteAvatars = Vue.extend({ addNoCommentClass() { const { notesCount } = this; - $(this.$el).closest('.js-avatar-container') + $(this.$el) + .closest('.js-avatar-container') .toggleClass('no-comment-btn', notesCount > 0) .nextUntil('.js-avatar-container') .toggleClass('no-comment-btn', notesCount > 0); }, toggleDiscussionsToggleState() { - const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); + const $notesHolders = $(this.$el) + .closest('.code') + .find('.notes_holder'); const $visibleNotesHolders = $notesHolders.filter(':visible'); - const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments'); - - $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length); + const $toggleDiffCommentsBtn = $(this.$el) + .closest('.diff-file') + .find('.js-toggle-diff-comments'); + + $toggleDiffCommentsBtn.toggleClass( + 'active', + $notesHolders.length === $visibleNotesHolders.length, + ); }, setDiscussionVisible() { - this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); + this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is( + ':visible', + ); }, getTooltipText(note) { return `${note.authorName}: ${note.noteTruncated}`; diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 2b78bb58735..c0c21416275 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -14,24 +14,24 @@ const JumpToDiscussion = Vue.extend({ required: true, }, }, - data: function () { + data: function() { return { discussions: CommentsStore.state, discussion: {}, }; }, computed: { - buttonText: function () { + buttonText: function() { if (this.discussionId) { return 'Jump to next unresolved discussion'; } else { return 'Jump to first unresolved discussion'; } }, - allResolved: function () { + allResolved: function() { return this.unresolvedDiscussionCount === 0; }, - showButton: function () { + showButton: function() { if (this.discussionId) { if (this.unresolvedDiscussionCount > 1) { return true; @@ -42,7 +42,7 @@ const JumpToDiscussion = Vue.extend({ return this.unresolvedDiscussionCount >= 1; } }, - lastResolvedId: function () { + lastResolvedId: function() { let lastId; for (const discussionId in this.discussions) { const discussion = this.discussions[discussionId]; @@ -52,13 +52,13 @@ const JumpToDiscussion = Vue.extend({ } } return lastId; - } + }, }, created() { this.discussion = this.discussions[this.discussionId]; }, methods: { - jumpToNextUnresolvedDiscussion: function () { + jumpToNextUnresolvedDiscussion: function() { let discussionsSelector; let discussionIdsInScope; let firstUnresolvedDiscussionId; @@ -68,9 +68,11 @@ const JumpToDiscussion = Vue.extend({ let jumpToFirstDiscussion = !this.discussionId; const discussionIdsForElements = function(elements) { - return elements.map(function() { - return $(this).attr('data-discussion-id'); - }).toArray(); + return elements + .map(function() { + return $(this).attr('data-discussion-id'); + }) + .toArray(); }; const { discussions } = this; @@ -144,8 +146,7 @@ const JumpToDiscussion = Vue.extend({ if (!discussion.isResolved()) { nextUnresolvedDiscussionId = discussionId; break; - } - else { + } else { continue; } } @@ -175,9 +176,9 @@ const JumpToDiscussion = Vue.extend({ // Resolved discussions are hidden in the diffs tab by default. // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab. // When jumping between unresolved discussions on the diffs tab, we show them. - $target.closest(".content").show(); + $target.closest('.content').show(); - const $notesHolder = $target.closest("tr.notes_holder"); + const $notesHolder = $target.closest('tr.notes_holder'); // Image diff discussions does not use notes_holder // so we should keep original $target value in those cases @@ -194,7 +195,7 @@ const JumpToDiscussion = Vue.extend({ prevEl = $target.prev(); // If the discussion doesn't have 4 lines above it, we'll have to do with fewer. - if (!prevEl.hasClass("line_holder")) { + if (!prevEl.hasClass('line_holder')) { break; } @@ -203,9 +204,9 @@ const JumpToDiscussion = Vue.extend({ } $.scrollTo($target, { - offset: -150 + offset: -150, }); - } + }, }, }); diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js index eb539c6b348..d8b056096f4 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js @@ -13,17 +13,17 @@ window.ResolveCount = Vue.extend({ required: true, }, }, - data: function () { + data: function() { return { - discussions: CommentsStore.state + discussions: CommentsStore.state, }; }, computed: { - allResolved: function () { + allResolved: function() { return this.resolvedDiscussionCount === this.discussionCount; }, resolvedCountText() { return this.discussionCount === 1 ? 'discussion' : 'discussions'; - } - } + }, + }, }); diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js b/app/assets/javascripts/diff_notes/mixins/discussion.js index 7589f9dd6e0..dea64dca132 100644 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js +++ b/app/assets/javascripts/diff_notes/mixins/discussion.js @@ -2,10 +2,10 @@ const DiscussionMixins = { computed: { - discussionCount: function () { + discussionCount: function() { return Object.keys(this.discussions).length; }, - resolvedDiscussionCount: function () { + resolvedDiscussionCount: function() { let resolvedCount = 0; for (const discussionId in this.discussions) { @@ -18,7 +18,7 @@ const DiscussionMixins = { return resolvedCount; }, - unresolvedDiscussionCount: function () { + unresolvedDiscussionCount: function() { let unresolvedCount = 0; for (const discussionId in this.discussions) { @@ -30,8 +30,8 @@ const DiscussionMixins = { } return unresolvedCount; - } - } + }, + }, }; export default DiscussionMixins; diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js index 787e6d8855f..daf61e5d467 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js +++ b/app/assets/javascripts/diff_notes/models/discussion.js @@ -6,22 +6,22 @@ import Vue from 'vue'; import { localTimeAgo } from '../../lib/utils/datetime_utility'; class DiscussionModel { - constructor (discussionId) { + constructor(discussionId) { this.id = discussionId; this.notes = {}; this.loading = false; this.canResolve = false; } - createNote (noteObj) { + createNote(noteObj) { Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj)); } - deleteNote (noteId) { + deleteNote(noteId) { Vue.delete(this.notes, noteId); } - getNote (noteId) { + getNote(noteId) { return this.notes[noteId]; } @@ -29,7 +29,7 @@ class DiscussionModel { return Object.keys(this.notes).length; } - isResolved () { + isResolved() { for (const noteId in this.notes) { const note = this.notes[noteId]; @@ -40,7 +40,7 @@ class DiscussionModel { return true; } - resolveAllNotes (resolved_by) { + resolveAllNotes(resolved_by) { for (const noteId in this.notes) { const note = this.notes[noteId]; @@ -51,7 +51,7 @@ class DiscussionModel { } } - unResolveAllNotes () { + unResolveAllNotes() { for (const noteId in this.notes) { const note = this.notes[noteId]; @@ -62,7 +62,7 @@ class DiscussionModel { } } - updateHeadline (data) { + updateHeadline(data) { const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`; const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`); @@ -79,7 +79,7 @@ class DiscussionModel { } } - isResolvable () { + isResolvable() { if (!this.canResolve) { return false; } diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js index d012cd02d10..060bb044f78 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js +++ b/app/assets/javascripts/diff_notes/stores/comments.js @@ -5,10 +5,10 @@ import Vue from 'vue'; window.CommentsStore = { state: {}, - get: function (discussionId, noteId) { + get: function(discussionId, noteId) { return this.state[discussionId].getNote(noteId); }, - createDiscussion: function (discussionId, canResolve) { + createDiscussion: function(discussionId, canResolve) { let discussion = this.state[discussionId]; if (!this.state[discussionId]) { discussion = new DiscussionModel(discussionId); @@ -21,18 +21,18 @@ window.CommentsStore = { return discussion; }, - create: function (noteObj) { + create: function(noteObj) { const discussion = this.createDiscussion(noteObj.discussionId); discussion.createNote(noteObj); }, - update: function (discussionId, noteId, resolved, resolved_by) { + update: function(discussionId, noteId, resolved, resolved_by) { const discussion = this.state[discussionId]; const note = discussion.getNote(noteId); note.resolved = resolved; note.resolved_by = resolved_by; }, - delete: function (discussionId, noteId) { + delete: function(discussionId, noteId) { const discussion = this.state[discussionId]; discussion.deleteNote(noteId); @@ -40,7 +40,7 @@ window.CommentsStore = { Vue.delete(this.state, discussionId); } }, - unresolvedDiscussionIds: function () { + unresolvedDiscussionIds: function() { const ids = []; for (const discussionId in this.state) { @@ -52,5 +52,5 @@ window.CommentsStore = { } return ids; - } + }, }; diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue index 993206b2e73..23d0bad2ecb 100644 --- a/app/assets/javascripts/diffs/components/commit_item.vue +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -43,7 +43,9 @@ export default { return (this.commit.author && this.commit.author.name) || this.commit.authorName; }, authorUrl() { - return (this.commit.author && this.commit.author.webUrl) || `mailto:${this.commit.authorEmail}`; + return ( + (this.commit.author && this.commit.author.webUrl) || `mailto:${this.commit.authorEmail}` + ); }, authorAvatar() { return (this.commit.author && this.commit.author.avatarUrl) || this.commit.authorGravatarUrl; diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue index c3acc352d5e..f4b333f3700 100644 --- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue +++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue @@ -115,7 +115,7 @@ export default { <span> {{ selectedVersionName }} </span> - <Icon + <icon :size="12" name="angle-down" class="position-absolute" diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 4e04e50c52a..f72c7a84e5c 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -46,10 +46,10 @@ export default { showExpandMessage() { return ( this.isCollapsed || - !this.file.highlightedDiffLines && - !this.isLoadingCollapsedDiff && - !this.file.tooLarge && - this.file.text + (!this.file.highlightedDiffLines && + !this.isLoadingCollapsedDiff && + !this.file.tooLarge && + this.file.text) ); }, showLoadingIcon() { diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_collection.js b/app/assets/javascripts/dirty_submit/dirty_submit_collection.js new file mode 100644 index 00000000000..42b051b2270 --- /dev/null +++ b/app/assets/javascripts/dirty_submit/dirty_submit_collection.js @@ -0,0 +1,13 @@ +import DirtySubmitForm from './dirty_submit_form'; + +class DirtySubmitCollection { + constructor(forms) { + this.forms = forms; + + this.dirtySubmits = []; + + this.forms.forEach(form => this.dirtySubmits.push(new DirtySubmitForm(form))); + } +} + +export default DirtySubmitCollection; diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_factory.js b/app/assets/javascripts/dirty_submit/dirty_submit_factory.js new file mode 100644 index 00000000000..95a896a7f0b --- /dev/null +++ b/app/assets/javascripts/dirty_submit/dirty_submit_factory.js @@ -0,0 +1,9 @@ +import DirtySubmitCollection from './dirty_submit_collection'; +import DirtySubmitForm from './dirty_submit_form'; + +export default function dirtySubmitFactory(formOrForms) { + const isCollection = formOrForms instanceof NodeList || formOrForms instanceof Array; + const DirtySubmitClass = isCollection ? DirtySubmitCollection : DirtySubmitForm; + + return new DirtySubmitClass(formOrForms); +} diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js new file mode 100644 index 00000000000..5bea47f23c5 --- /dev/null +++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js @@ -0,0 +1,82 @@ +import _ from 'underscore'; + +class DirtySubmitForm { + constructor(form) { + this.form = form; + this.dirtyInputs = []; + this.isDisabled = true; + + this.init(); + } + + init() { + this.inputs = this.form.querySelectorAll('input, textarea, select'); + this.submits = this.form.querySelectorAll('input[type=submit], button[type=submit]'); + + this.inputs.forEach(DirtySubmitForm.initInput); + this.toggleSubmission(); + + this.registerListeners(); + } + + registerListeners() { + const throttledUpdateDirtyInput = _.throttle( + event => this.updateDirtyInput(event), + DirtySubmitForm.THROTTLE_DURATION, + ); + this.form.addEventListener('input', throttledUpdateDirtyInput); + this.form.addEventListener('submit', event => this.formSubmit(event)); + } + + updateDirtyInput(event) { + const input = event.target; + + if (!input.dataset.dirtySubmitOriginalValue) return; + + this.updateDirtyInputs(input); + this.toggleSubmission(); + } + + updateDirtyInputs(input) { + const { name } = input; + const isDirty = + input.dataset.dirtySubmitOriginalValue !== DirtySubmitForm.inputCurrentValue(input); + const indexOfInputName = this.dirtyInputs.indexOf(name); + const isExisting = indexOfInputName !== -1; + + if (isDirty && !isExisting) this.dirtyInputs.push(name); + if (!isDirty && isExisting) this.dirtyInputs.splice(indexOfInputName, 1); + } + + toggleSubmission() { + this.isDisabled = this.dirtyInputs.length === 0; + this.submits.forEach(element => { + element.disabled = this.isDisabled; + }); + } + + formSubmit(event) { + if (this.isDisabled) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + + return !this.isDisabled; + } + + static initInput(element) { + element.dataset.dirtySubmitOriginalValue = DirtySubmitForm.inputCurrentValue(element); + } + + static isInputCheckable(input) { + return input.type === 'checkbox' || input.type === 'radio'; + } + + static inputCurrentValue(input) { + return DirtySubmitForm.isInputCheckable(input) ? input.checked.toString() : input.value; + } +} + +DirtySubmitForm.THROTTLE_DURATION = 500; + +export default DirtySubmitForm; diff --git a/app/assets/javascripts/dismissable_callout.js b/app/assets/javascripts/dismissable_callout.js new file mode 100644 index 00000000000..5185b019376 --- /dev/null +++ b/app/assets/javascripts/dismissable_callout.js @@ -0,0 +1,27 @@ +import $ from 'jquery'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import Flash from '~/flash'; + +export default function initDismissableCallout(alertSelector) { + const alertEl = document.querySelector(alertSelector); + if (!alertEl) { + return; + } + + const closeButtonEl = alertEl.getElementsByClassName('close')[0]; + const { dismissEndpoint, featureId } = closeButtonEl.dataset; + + closeButtonEl.addEventListener('click', () => { + axios + .post(dismissEndpoint, { + feature_name: featureId, + }) + .then(() => { + $(alertEl).alert('close'); + }) + .catch(() => { + Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.')); + }); + }); +} diff --git a/app/assets/javascripts/droplab/constants.js b/app/assets/javascripts/droplab/constants.js index 868d47e91b3..6451af49d36 100644 --- a/app/assets/javascripts/droplab/constants.js +++ b/app/assets/javascripts/droplab/constants.js @@ -6,11 +6,4 @@ const IGNORE_CLASS = 'droplab-item-ignore'; // Matches `{{anything}}` and `{{ everything }}`. const TEMPLATE_REGEX = /\{\{(.+?)\}\}/g; -export { - DATA_TRIGGER, - DATA_DROPDOWN, - SELECTED_CLASS, - ACTIVE_CLASS, - TEMPLATE_REGEX, - IGNORE_CLASS, -}; +export { DATA_TRIGGER, DATA_DROPDOWN, SELECTED_CLASS, ACTIVE_CLASS, TEMPLATE_REGEX, IGNORE_CLASS }; diff --git a/app/assets/javascripts/droplab/drop_down.js b/app/assets/javascripts/droplab/drop_down.js index 3cc316c3f3e..ccb3d56ed8c 100644 --- a/app/assets/javascripts/droplab/drop_down.js +++ b/app/assets/javascripts/droplab/drop_down.js @@ -2,7 +2,7 @@ import utils from './utils'; import { SELECTED_CLASS, IGNORE_CLASS } from './constants'; class DropDown { - constructor(list, config = { }) { + constructor(list, config = {}) { this.currentIndex = 0; this.hidden = true; this.list = typeof list === 'string' ? document.querySelector(list) : list; @@ -157,7 +157,7 @@ class DropDown { static setImagesSrc(template) { const images = [...template.querySelectorAll('img[data-src]')]; - images.forEach((image) => { + images.forEach(image => { const img = image; img.src = img.getAttribute('data-src'); diff --git a/app/assets/javascripts/droplab/drop_lab.js b/app/assets/javascripts/droplab/drop_lab.js index 2a02ede72bf..1339e28d8b8 100644 --- a/app/assets/javascripts/droplab/drop_lab.js +++ b/app/assets/javascripts/droplab/drop_lab.js @@ -51,7 +51,7 @@ class DropLab { } processData(trigger, data, methodName) { - this.hooks.forEach((hook) => { + this.hooks.forEach(hook => { if (Array.isArray(trigger)) hook.list[methodName](trigger); if (hook.trigger.id === trigger) hook.list[methodName](data); @@ -78,7 +78,8 @@ class DropLab { } changeHookList(trigger, list, plugins, config) { - const availableTrigger = typeof trigger === 'string' ? document.getElementById(trigger) : trigger; + const availableTrigger = + typeof trigger === 'string' ? document.getElementById(trigger) : trigger; this.hooks.forEach((hook, i) => { const aHook = hook; diff --git a/app/assets/javascripts/droplab/keyboard.js b/app/assets/javascripts/droplab/keyboard.js index 02f1b805ce4..40837ffdf8f 100644 --- a/app/assets/javascripts/droplab/keyboard.js +++ b/app/assets/javascripts/droplab/keyboard.js @@ -2,15 +2,18 @@ import { ACTIVE_CLASS } from './constants'; -const Keyboard = function () { +const Keyboard = function() { var currentKey; var currentFocus; var isUpArrow = false; var isDownArrow = false; var removeHighlight = function removeHighlight(list) { - var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider):not(.hidden)'), 0); + var itemElements = Array.prototype.slice.call( + list.list.querySelectorAll('li:not(.divider):not(.hidden)'), + 0, + ); var listItems = []; - for(var i = 0; i < itemElements.length; i++) { + for (var i = 0; i < itemElements.length; i++) { var listItem = itemElements[i]; listItem.classList.remove(ACTIVE_CLASS); @@ -23,13 +26,13 @@ const Keyboard = function () { var setMenuForArrows = function setMenuForArrows(list) { var listItems = removeHighlight(list); - if(list.currentIndex>0){ - if(!listItems[list.currentIndex-1]){ - list.currentIndex = list.currentIndex-1; + if (list.currentIndex > 0) { + if (!listItems[list.currentIndex - 1]) { + list.currentIndex = list.currentIndex - 1; } - if (listItems[list.currentIndex-1]) { - var el = listItems[list.currentIndex-1]; + if (listItems[list.currentIndex - 1]) { + var el = listItems[list.currentIndex - 1]; var filterDropdownEl = el.closest('.filter-dropdown'); el.classList.add(ACTIVE_CLASS); @@ -55,7 +58,7 @@ const Keyboard = function () { }; var selectItem = function selectItem(list) { var listItems = removeHighlight(list); - var currentItem = listItems[list.currentIndex-1]; + var currentItem = listItems[list.currentIndex - 1]; var listEvent = new CustomEvent('click.dl', { detail: { list: list, @@ -65,43 +68,49 @@ const Keyboard = function () { }); list.list.dispatchEvent(listEvent); list.hide(); - } + }; - var keydown = function keydown(e){ + var keydown = function keydown(e) { var typedOn = e.target; var list = e.detail.hook.list; var currentIndex = list.currentIndex; isUpArrow = false; isDownArrow = false; - if(e.detail.which){ + if (e.detail.which) { currentKey = e.detail.which; - if(currentKey === 13){ + if (currentKey === 13) { selectItem(e.detail.hook.list); return; } - if(currentKey === 38) { + if (currentKey === 38) { isUpArrow = true; } - if(currentKey === 40) { + if (currentKey === 40) { isDownArrow = true; } - } else if(e.detail.key) { + } else if (e.detail.key) { currentKey = e.detail.key; - if(currentKey === 'Enter'){ + if (currentKey === 'Enter') { selectItem(e.detail.hook.list); return; } - if(currentKey === 'ArrowUp') { + if (currentKey === 'ArrowUp') { isUpArrow = true; } - if(currentKey === 'ArrowDown') { + if (currentKey === 'ArrowDown') { isDownArrow = true; } } - if(isUpArrow){ currentIndex--; } - if(isDownArrow){ currentIndex++; } - if(currentIndex < 0){ currentIndex = 0; } + if (isUpArrow) { + currentIndex--; + } + if (isDownArrow) { + currentIndex++; + } + if (currentIndex < 0) { + currentIndex = 0; + } list.currentIndex = currentIndex; setMenuForArrows(e.detail.hook.list); }; diff --git a/app/assets/javascripts/droplab/plugins/ajax.js b/app/assets/javascripts/droplab/plugins/ajax.js index 267b53fa4f2..48b2a90c459 100644 --- a/app/assets/javascripts/droplab/plugins/ajax.js +++ b/app/assets/javascripts/droplab/plugins/ajax.js @@ -43,12 +43,12 @@ const Ajax = { return AjaxCache.retrieve(config.endpoint) .then(self.preprocessing.bind(null, config)) - .then((data) => self._loadData(data, config, self)) + .then(data => self._loadData(data, config, self)) .catch(config.onError); }, destroy: function() { this.destroyed = true; - } + }, }; export default Ajax; diff --git a/app/assets/javascripts/droplab/plugins/ajax_filter.js b/app/assets/javascripts/droplab/plugins/ajax_filter.js index 1db20227a16..66a52548417 100644 --- a/app/assets/javascripts/droplab/plugins/ajax_filter.js +++ b/app/assets/javascripts/droplab/plugins/ajax_filter.js @@ -41,8 +41,10 @@ const AjaxFilter = { if (config.searchValueFunction) { searchValue = config.searchValueFunction(); } - if (config.loadingTemplate && this.hook.list.data === undefined || - this.hook.list.data.length === 0) { + if ( + (config.loadingTemplate && this.hook.list.data === undefined) || + this.hook.list.data.length === 0 + ) { var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); var loadingTemplate = document.createElement('div'); loadingTemplate.innerHTML = config.loadingTemplate; @@ -61,7 +63,7 @@ const AjaxFilter = { params[config.searchKey] = searchValue; var url = config.endpoint + this.buildParams(params); return AjaxCache.retrieve(url) - .then((data) => { + .then(data => { this._loadData(data, config); if (config.onLoadingFinished) { config.onLoadingFinished(data); @@ -72,8 +74,7 @@ const AjaxFilter = { _loadData(data, config) { const list = this.hook.list; - if (config.loadingTemplate && list.data === undefined || - list.data.length === 0) { + if ((config.loadingTemplate && list.data === undefined) || list.data.length === 0) { const dataLoadingTemplate = list.list.querySelector('[data-loading-template]'); if (dataLoadingTemplate) { dataLoadingTemplate.outerHTML = this.listTemplate; @@ -81,7 +82,8 @@ const AjaxFilter = { } if (!this.destroyed) { var hookListChildren = list.list.children; - var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); + var onlyDynamicList = + hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); if (onlyDynamicList && data.length === 0) { list.hide(); } @@ -100,12 +102,12 @@ const AjaxFilter = { }, destroy: function destroy() { - if (this.timeout)clearTimeout(this.timeout); + if (this.timeout) clearTimeout(this.timeout); this.destroyed = true; this.hook.trigger.removeEventListener('keydown.dl', this.eventWrapper.debounceTrigger); this.hook.trigger.removeEventListener('focus', this.eventWrapper.debounceTrigger); - } + }, }; export default AjaxFilter; diff --git a/app/assets/javascripts/droplab/plugins/filter.js b/app/assets/javascripts/droplab/plugins/filter.js index 404d707cf7a..6f1dc252d24 100644 --- a/app/assets/javascripts/droplab/plugins/filter.js +++ b/app/assets/javascripts/droplab/plugins/filter.js @@ -1,7 +1,7 @@ /* eslint-disable */ const Filter = { - keydown: function(e){ + keydown: function(e) { if (this.destroyed) return; var hiddenCount = 0; @@ -14,14 +14,14 @@ const Filter = { var matches = []; var filterFunction; // will only work on dynamically set data - if(!data){ + if (!data) { return; } if (config && config.filterFunction && typeof config.filterFunction === 'function') { filterFunction = config.filterFunction; } else { - filterFunction = function(o){ + filterFunction = function(o) { // cheap string search o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1; return o; @@ -47,20 +47,23 @@ const Filter = { }, debounceKeydown: function debounceKeydown(e) { - if ([ - 13, // enter - 16, // shift - 17, // ctrl - 18, // alt - 20, // caps lock - 37, // left arrow - 38, // up arrow - 39, // right arrow - 40, // down arrow - 91, // left window - 92, // right window - 93, // select - ].indexOf(e.detail.which || e.detail.keyCode) > -1) return; + if ( + [ + 13, // enter + 16, // shift + 17, // ctrl + 18, // alt + 20, // caps lock + 37, // left arrow + 38, // up arrow + 39, // right arrow + 40, // down arrow + 91, // left window + 92, // right window + 93, // select + ].indexOf(e.detail.which || e.detail.keyCode) > -1 + ) + return; if (this.timeout) clearTimeout(this.timeout); this.timeout = setTimeout(this.keydown.bind(this, e), 200); @@ -87,7 +90,7 @@ const Filter = { this.hook.trigger.removeEventListener('keydown.dl', this.eventWrapper.debounceKeydown); this.hook.trigger.removeEventListener('mousedown.dl', this.eventWrapper.debounceKeydown); - } + }, }; export default Filter; diff --git a/app/assets/javascripts/droplab/plugins/input_setter.js b/app/assets/javascripts/droplab/plugins/input_setter.js index d01fbc5830d..6cfc738a1e3 100644 --- a/app/assets/javascripts/droplab/plugins/input_setter.js +++ b/app/assets/javascripts/droplab/plugins/input_setter.js @@ -36,8 +36,8 @@ const InputSetter = { const inputAttribute = config.inputAttribute; if (input.hasAttribute(inputAttribute)) return input.setAttribute(inputAttribute, newValue); - if (input.tagName === 'INPUT') return input.value = newValue; - return input.textContent = newValue; + if (input.tagName === 'INPUT') return (input.value = newValue); + return (input.textContent = newValue); }, destroy() { diff --git a/app/assets/javascripts/droplab/utils.js b/app/assets/javascripts/droplab/utils.js index bfe056a0fcc..5272778ce2d 100644 --- a/app/assets/javascripts/droplab/utils.js +++ b/app/assets/javascripts/droplab/utils.js @@ -5,7 +5,12 @@ import { DATA_TRIGGER, DATA_DROPDOWN, TEMPLATE_REGEX } from './constants'; const utils = { toCamelCase(attr) { - return this.camelize(attr.split('-').slice(1).join(' ')); + return this.camelize( + attr + .split('-') + .slice(1) + .join(' '), + ); }, template(templateString, data) { @@ -17,9 +22,11 @@ const utils = { }, camelize(str) { - return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => { - return index === 0 ? letter.toLowerCase() : letter.toUpperCase(); - }).replace(/\s+/g, ''); + return str + .replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => { + return index === 0 ? letter.toLowerCase() : letter.toUpperCase(); + }) + .replace(/\s+/g, ''); }, closest(thisTag, stopTag) { diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index 9de851c9409..00d197d294f 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -1,40 +1,40 @@ <script> - import tablePagination from '../../vue_shared/components/table_pagination.vue'; - import environmentTable from '../components/environments_table.vue'; +import tablePagination from '../../vue_shared/components/table_pagination.vue'; +import environmentTable from '../components/environments_table.vue'; - export default { - components: { - environmentTable, - tablePagination, +export default { + components: { + environmentTable, + tablePagination, + }, + props: { + isLoading: { + type: Boolean, + required: true, }, - props: { - isLoading: { - type: Boolean, - required: true, - }, - environments: { - type: Array, - required: true, - }, - pagination: { - type: Object, - required: true, - }, - canCreateDeployment: { - type: Boolean, - required: true, - }, - canReadEnvironment: { - type: Boolean, - required: true, - }, + environments: { + type: Array, + required: true, }, - methods: { - onChangePage(page) { - this.$emit('onChangePage', page); - }, + pagination: { + type: Object, + required: true, }, - }; + canCreateDeployment: { + type: Boolean, + required: true, + }, + canReadEnvironment: { + type: Boolean, + required: true, + }, + }, + methods: { + onChangePage(page) { + this.$emit('onChangePage', page); + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue index cf78f89981e..2360a52645b 100644 --- a/app/assets/javascripts/environments/components/empty_state.vue +++ b/app/assets/javascripts/environments/components/empty_state.vue @@ -1,44 +1,45 @@ <script> - export default { - name: 'EnvironmentsEmptyState', - props: { - newPath: { - type: String, - required: true, - }, - canCreateEnvironment: { - type: Boolean, - required: true, - }, - helpPath: { - type: String, - required: true, - }, +export default { + name: 'EnvironmentsEmptyState', + props: { + newPath: { + type: String, + required: true, }, - }; + canCreateEnvironment: { + type: Boolean, + required: true, + }, + helpPath: { + type: String, + required: true, + }, + }, +}; </script> <template> - <div class="blank-state-row"> - <div class="blank-state-center"> - <h2 class="blank-state-title js-blank-state-title"> - {{ s__("Environments|You don't have any environments right now.") }} - </h2> + <div class="empty-state"> + <div class="text-content"> + <h4 class="blank-state-title js-blank-state-title"> + {{ s__("Environments|You don't have any environments right now") }} + </h4> <p class="blank-state-text"> {{ s__(`Environments|Environments are places where -code gets deployed, such as staging or production.`) }} - <br /> + code gets deployed, such as staging or production.`) }} <a :href="helpPath"> {{ s__("Environments|Read more about environments") }} </a> </p> - <a - v-if="canCreateEnvironment" - :href="newPath" - class="btn btn-success js-new-environment-button" - > - {{ s__("Environments|New environment") }} - </a> + <div class="text-center"> + <a + v-if="canCreateEnvironment" + :href="newPath" + class="btn btn-success js-new-environment-button" + > + {{ s__("Environments|New environment") }} + </a> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index e1f9248bc4c..2bc168a6b02 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -60,11 +60,9 @@ export default { > <span> <icon name="play" /> - <i - class="fa fa-caret-down" - aria-hidden="true" - > - </i> + <icon + name="chevron-down" + /> <gl-loading-icon v-if="isLoading" /> </span> </button> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index ad5d16874f3..bb9c139727e 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -4,6 +4,7 @@ import _ from 'underscore'; import tooltip from '~/vue_shared/directives/tooltip'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; import { humanize } from '~/lib/utils/text_utility'; +import Icon from '~/vue_shared/components/icon.vue'; import ActionsComponent from './environment_actions.vue'; import ExternalUrlComponent from './environment_external_url.vue'; import StopComponent from './environment_stop.vue'; @@ -24,6 +25,7 @@ export default { components: { UserAvatarLink, CommitComponent, + Icon, ActionsComponent, ExternalUrlComponent, StopComponent, @@ -448,6 +450,10 @@ export default { this.canRetry ); }, + + folderIconName() { + return this.model.isOpen ? 'chevron-down' : 'chevron-right'; + }, }, methods: { @@ -494,27 +500,15 @@ export default { role="button" @click="onClickFolder"> - <span class="folder-icon"> - <i - v-show="model.isOpen" - class="fa fa-caret-down" - aria-hidden="true" - > - </i> - <i - v-show="!model.isOpen" - class="fa fa-caret-right" - aria-hidden="true" - > - </i> - </span> + <icon + :name="folderIconName" + class="folder-icon" + /> - <span class="folder-icon"> - <i - class="fa fa-folder" - aria-hidden="true"> - </i> - </span> + <icon + name="folder" + class="folder-icon" + /> <span> {{ model.folderName }} diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index efbf88d0f11..9e137f79dcc 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -38,7 +38,9 @@ export default { computed: { title() { - return this.isLastDeployment ? s__('Environments|Re-deploy to environment') : s__('Environments|Rollback environment'); + return this.isLastDeployment + ? s__('Environments|Re-deploy to environment') + : s__('Environments|Rollback environment'); }, }, diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index de0fbdb2e91..f044d31c776 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -5,31 +5,32 @@ import Translate from '../../vue_shared/translate'; Vue.use(Translate); -export default () => new Vue({ - el: '#environments-folder-list-view', - components: { - environmentsFolderApp, - }, - data() { - const environmentsData = document.querySelector(this.$options.el).dataset; +export default () => + new Vue({ + el: '#environments-folder-list-view', + components: { + environmentsFolderApp, + }, + data() { + const environmentsData = document.querySelector(this.$options.el).dataset; - return { - endpoint: environmentsData.endpoint, - folderName: environmentsData.folderName, - cssContainerClass: environmentsData.cssClass, - canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment), - canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment), - }; - }, - render(createElement) { - return createElement('environments-folder-app', { - props: { - endpoint: this.endpoint, - folderName: this.folderName, - cssContainerClass: this.cssContainerClass, - canCreateDeployment: this.canCreateDeployment, - canReadEnvironment: this.canReadEnvironment, - }, - }); - }, -}); + return { + endpoint: environmentsData.endpoint, + folderName: environmentsData.folderName, + cssContainerClass: environmentsData.cssClass, + canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment), + canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment), + }; + }, + render(createElement) { + return createElement('environments-folder-app', { + props: { + endpoint: this.endpoint, + folderName: this.folderName, + cssContainerClass: this.cssContainerClass, + canCreateDeployment: this.canCreateDeployment, + canReadEnvironment: this.canReadEnvironment, + }, + }); + }, + }); diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index e69bfa0b2cc..6be4845fe4c 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -1,46 +1,43 @@ <script> - import environmentsMixin from '../mixins/environments_mixin'; - import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; - import StopEnvironmentModal from '../components/stop_environment_modal.vue'; +import environmentsMixin from '../mixins/environments_mixin'; +import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; +import StopEnvironmentModal from '../components/stop_environment_modal.vue'; - export default { - components: { - StopEnvironmentModal, - }, +export default { + components: { + StopEnvironmentModal, + }, - mixins: [ - environmentsMixin, - CIPaginationMixin, - ], + mixins: [environmentsMixin, CIPaginationMixin], - props: { - endpoint: { - type: String, - required: true, - }, - folderName: { - type: String, - required: true, - }, - cssContainerClass: { - type: String, - required: true, - }, - canCreateDeployment: { - type: Boolean, - required: true, - }, - canReadEnvironment: { - type: Boolean, - required: true, - }, + props: { + endpoint: { + type: String, + required: true, + }, + folderName: { + type: String, + required: true, + }, + cssContainerClass: { + type: String, + required: true, + }, + canCreateDeployment: { + type: Boolean, + required: true, + }, + canReadEnvironment: { + type: Boolean, + required: true, }, - methods: { - successCallback(resp) { - this.saveData(resp); - }, + }, + methods: { + successCallback(resp) { + this.saveData(resp); }, - }; + }, +}; </script> <template> <div :class="cssContainerClass"> diff --git a/app/assets/javascripts/environments/index.js b/app/assets/javascripts/environments/index.js index afc4aba6554..5b6833fb15d 100644 --- a/app/assets/javascripts/environments/index.js +++ b/app/assets/javascripts/environments/index.js @@ -5,35 +5,36 @@ import Translate from '../vue_shared/translate'; Vue.use(Translate); -export default () => new Vue({ - el: '#environments-list-view', - components: { - environmentsComponent, - }, - data() { - const environmentsData = document.querySelector(this.$options.el).dataset; +export default () => + new Vue({ + el: '#environments-list-view', + components: { + environmentsComponent, + }, + data() { + const environmentsData = document.querySelector(this.$options.el).dataset; - return { - endpoint: environmentsData.environmentsDataEndpoint, - newEnvironmentPath: environmentsData.newEnvironmentPath, - helpPagePath: environmentsData.helpPagePath, - cssContainerClass: environmentsData.cssClass, - canCreateEnvironment: convertPermissionToBoolean(environmentsData.canCreateEnvironment), - canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment), - canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment), - }; - }, - render(createElement) { - return createElement('environments-component', { - props: { - endpoint: this.endpoint, - newEnvironmentPath: this.newEnvironmentPath, - helpPagePath: this.helpPagePath, - cssContainerClass: this.cssContainerClass, - canCreateEnvironment: this.canCreateEnvironment, - canCreateDeployment: this.canCreateDeployment, - canReadEnvironment: this.canReadEnvironment, - }, - }); - }, -}); + return { + endpoint: environmentsData.environmentsDataEndpoint, + newEnvironmentPath: environmentsData.newEnvironmentPath, + helpPagePath: environmentsData.helpPagePath, + cssContainerClass: environmentsData.cssClass, + canCreateEnvironment: convertPermissionToBoolean(environmentsData.canCreateEnvironment), + canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment), + canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment), + }; + }, + render(createElement) { + return createElement('environments-component', { + props: { + endpoint: this.endpoint, + newEnvironmentPath: this.newEnvironmentPath, + helpPagePath: this.helpPagePath, + cssContainerClass: this.cssContainerClass, + canCreateEnvironment: this.canCreateEnvironment, + canCreateDeployment: this.canCreateDeployment, + canReadEnvironment: this.canReadEnvironment, + }, + }); + }, + }); diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index d71964612c5..96dc1f07cb9 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -4,9 +4,7 @@ import _ from 'underscore'; import Visibility from 'visibilityjs'; import Poll from '../../lib/utils/poll'; -import { - getParameterByName, -} from '../../lib/utils/common_utils'; +import { getParameterByName } from '../../lib/utils/common_utils'; import { s__ } from '../../locale'; import Flash from '../../flash'; import eventHub from '../event_hub'; @@ -19,7 +17,6 @@ import tabs from '../../vue_shared/components/navigation_tabs.vue'; import container from '../components/container.vue'; export default { - components: { environmentTable, container, @@ -65,7 +62,8 @@ export default { updateContent(parameters) { this.updateInternalState(parameters); // fetch new data - return this.service.fetchEnvironments(this.requestData) + return this.service + .fetchEnvironments(this.requestData) .then(response => this.successCallback(response)) .then(() => { // restart polling @@ -88,7 +86,8 @@ export default { if (!this.isMakingRequest) { this.isLoading = true; - this.service.postAction(endpoint) + this.service + .postAction(endpoint) .then(() => this.fetchEnvironments()) .catch(() => { this.isLoading = false; @@ -100,7 +99,8 @@ export default { fetchEnvironments() { this.isLoading = true; - return this.service.fetchEnvironments(this.requestData) + return this.service + .fetchEnvironments(this.requestData) .then(this.successCallback) .catch(this.errorCallback); }, @@ -111,7 +111,9 @@ export default { stopEnvironment(environment) { const endpoint = environment.stop_path; - const errorMessage = s__('Environments|An error occurred while stopping the environment, please try again'); + const errorMessage = s__( + 'Environments|An error occurred while stopping the environment, please try again', + ); this.postAction({ endpoint, errorMessage }); }, }, @@ -149,7 +151,7 @@ export default { data: this.requestData, successCallback: this.successCallback, errorCallback: this.errorCallback, - notificationCallback: (isMakingRequest) => { + notificationCallback: isMakingRequest => { this.isMakingRequest = isMakingRequest; }, }); diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js index 03dfa942d69..173fe7c69de 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -1,13 +1,6 @@ import $ from 'jquery'; -import { - getSelector, - inserted, -} from './feature_highlight_helper'; -import { - togglePopover, - mouseenter, - debouncedMouseleave, -} from '../shared/popover'; +import { getSelector, inserted } from './feature_highlight_helper'; +import { togglePopover, mouseenter, debouncedMouseleave } from '../shared/popover'; export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { const $selector = $(getSelector(id)); @@ -41,8 +34,9 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { export function findHighestPriorityFeature() { let priorityFeature; - const sortedFeatureEls = [].slice.call(document.querySelectorAll('.js-feature-highlight')).sort((a, b) => - (a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0)); + const sortedFeatureEls = [].slice + .call(document.querySelectorAll('.js-feature-highlight')) + .sort((a, b) => (a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0)); const [priorityFeatureEl] = sortedFeatureEls; if (priorityFeatureEl) { diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js index d5b97ebb264..fd9433b625c 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js @@ -8,10 +8,17 @@ import { togglePopover } from '../shared/popover'; export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; export function dismiss(highlightId) { - axios.post(this.attr('data-dismiss-endpoint'), { - feature_name: highlightId, - }) - .catch(() => Flash(__('An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.'))); + axios + .post(this.attr('data-dismiss-endpoint'), { + feature_name: highlightId, + }) + .catch(() => + Flash( + __( + 'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.', + ), + ), + ); togglePopover.call(this, false); this.hide(); @@ -23,8 +30,7 @@ export function inserted() { const $popover = $(this); const dismissWrapper = dismiss.bind($popover, highlightId); - $(`#${popoverId} .dismiss-feature-highlight`) - .on('click', dismissWrapper); + $(`#${popoverId} .dismiss-feature-highlight`).on('click', dismissWrapper); const lazyImg = $(`#${popoverId} .feature-highlight-illustration`)[0]; if (lazyImg) { diff --git a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js index d7aa4ce597f..934375023ba 100644 --- a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js @@ -1,20 +1,23 @@ import FilteredSearchTokenKeys from './filtered_search_token_keys'; -const tokenKeys = [{ - key: 'status', - type: 'string', - param: 'status', - symbol: '', - icon: 'messages', - tag: 'status', -}, { - key: 'type', - type: 'string', - param: 'type', - symbol: '', - icon: 'cube', - tag: 'type', -}]; +const tokenKeys = [ + { + key: 'status', + type: 'string', + param: 'status', + symbol: '', + icon: 'messages', + tag: 'status', + }, + { + key: 'type', + type: 'string', + param: 'type', + symbol: '', + icon: 'cube', + tag: 'type', + }, +]; const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys); diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue index 21b5ccdb613..b9bc5e6ed7f 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -21,9 +21,11 @@ export default { }, computed: { processedItems() { - return this.items.map((item) => { - const { tokens, searchToken } - = FilteredSearchTokenizer.processTokens(item, this.allowedKeys); + return this.items.map(item => { + const { tokens, searchToken } = FilteredSearchTokenizer.processTokens( + item, + this.allowedKeys, + ); const resultantTokens = tokens.map(token => ({ prefix: `${token.key}:`, diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js index 5ddd0e5e690..af7936a92fb 100644 --- a/app/assets/javascripts/filtered_search/dropdown_emoji.js +++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js @@ -24,8 +24,12 @@ export default class DropdownEmoji extends FilteredSearchDropdown { }; import(/* webpackChunkName: 'emoji' */ '~/emoji') - .then(({ glEmojiTag }) => { this.glEmojiTag = glEmojiTag; }) - .catch(() => { /* ignore error and leave emoji name in the search bar */ }); + .then(({ glEmojiTag }) => { + this.glEmojiTag = glEmojiTag; + }) + .catch(() => { + /* ignore error and leave emoji name in the search bar */ + }); this.unbindEvents(); this.bindEvents(); @@ -48,7 +52,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown { } itemClicked(e) { - super.itemClicked(e, (selected) => { + super.itemClicked(e, selected => { const name = selected.querySelector('.js-data-value').innerText.trim(); return DropdownUtils.getEscapedText(name); }); @@ -64,7 +68,7 @@ export default class DropdownEmoji extends FilteredSearchDropdown { // Replace empty gl-emoji tag to real content const dropdownItems = [...this.dropdown.querySelectorAll('.filter-dropdown-item')]; - dropdownItems.forEach((dropdownItem) => { + dropdownItems.forEach(dropdownItem => { const name = dropdownItem.querySelector('.js-data-value').innerText; const emojiTag = this.glEmojiTag(name); const emojiElement = dropdownItem.querySelector('gl-emoji'); @@ -73,7 +77,6 @@ export default class DropdownEmoji extends FilteredSearchDropdown { } init() { - this.droplab - .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); + this.droplab.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index c568f4e4ebf..1a1135ae929 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -41,8 +41,10 @@ export default class DropdownHint extends FilteredSearchDropdown { previousInputValues.forEach((value, index) => { searchTerms.push(value); - if (index === previousInputValues.length - 1 - && token.indexOf(value.toLowerCase()) !== -1) { + if ( + index === previousInputValues.length - 1 && + token.indexOf(value.toLowerCase()) !== -1 + ) { searchTerms.pop(); } }); @@ -64,13 +66,12 @@ export default class DropdownHint extends FilteredSearchDropdown { } renderContent() { - const dropdownData = this.tokenKeys.get() - .map(tokenKey => ({ - icon: `${gon.sprite_icons}#${tokenKey.icon}`, - hint: tokenKey.key, - tag: `:${tokenKey.tag}`, - type: tokenKey.type, - })); + const dropdownData = this.tokenKeys.get().map(tokenKey => ({ + icon: `${gon.sprite_icons}#${tokenKey.icon}`, + hint: tokenKey.key, + tag: `:${tokenKey.tag}`, + type: tokenKey.type, + })); this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config); this.droplab.setData(this.hookId, dropdownData); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index 2ffda7e2037..0264f934914 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -29,20 +29,18 @@ export default class DropdownNonUser extends FilteredSearchDropdown { } itemClicked(e) { - super.itemClicked(e, (selected) => { + super.itemClicked(e, selected => { const title = selected.querySelector('.js-data-value').innerText.trim(); return `${this.symbol}${DropdownUtils.getEscapedText(title)}`; }); } renderContent(forceShowList = false) { - this.droplab - .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config); + this.droplab.changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config); super.renderContent(forceShowList); } init() { - this.droplab - .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); + this.droplab.addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init(); } } diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 6da6ca10008..1b79a3320c6 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -41,7 +41,7 @@ export default class DropdownUtils { // Removes the first character if it is a quotation so that we can search // with multiple words - if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { + if ((value[0] === '"' || value[0] === "'") && title.indexOf(' ') !== -1) { value = value.slice(1); } @@ -82,11 +82,13 @@ export default class DropdownUtils { // Reduce the colors to 4 colors.length = Math.min(colors.length, 4); - const color = colors.map((c, i) => { - const percentFirst = Math.floor(spacing * i); - const percentSecond = Math.floor(spacing * (i + 1)); - return `${c} ${percentFirst}%, ${c} ${percentSecond}%`; - }).join(', '); + const color = colors + .map((c, i) => { + const percentFirst = Math.floor(spacing * i); + const percentSecond = Math.floor(spacing * (i + 1)); + return `${c} ${percentFirst}%, ${c} ${percentSecond}%`; + }) + .join(', '); return `linear-gradient(${color})`; } @@ -97,17 +99,16 @@ export default class DropdownUtils { data.forEach(DropdownUtils.mergeDuplicateLabels.bind(null, dataMap)); - Object.keys(dataMap) - .forEach((key) => { - const label = dataMap[key]; + Object.keys(dataMap).forEach(key => { + const label = dataMap[key]; - if (label.multipleColors) { - label.color = DropdownUtils.duplicateLabelColor(label.multipleColors); - label.text_color = '#000000'; - } + if (label.multipleColors) { + label.color = DropdownUtils.duplicateLabelColor(label.multipleColors); + label.text_color = '#000000'; + } - results.push(label); - }); + results.push(label); + }); results.preprocessed = true; @@ -118,8 +119,7 @@ export default class DropdownUtils { const { input, allowedKeys } = config; const updatedItem = item; const searchInput = DropdownUtils.getSearchQuery(input); - const { lastToken, tokens } = - FilteredSearchTokenizer.processTokens(searchInput, allowedKeys); + const { lastToken, tokens } = FilteredSearchTokenizer.processTokens(searchInput, allowedKeys); const lastKey = lastToken.key || lastToken || ''; const allowMultiple = item.type === 'array'; const itemInExistingTokens = tokens.some(t => t.key === item.hint); @@ -154,7 +154,10 @@ export default class DropdownUtils { static getVisualTokenValues(visualToken) { const tokenName = visualToken && visualToken.querySelector('.name').textContent.trim(); - let tokenValue = visualToken && visualToken.querySelector('.value') && visualToken.querySelector('.value').textContent.trim(); + let tokenValue = + visualToken && + visualToken.querySelector('.value') && + visualToken.querySelector('.value').textContent.trim(); if (tokenName === 'label' && tokenValue) { // remove leading symbol and wrapping quotes tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, ''); @@ -174,7 +177,7 @@ export default class DropdownUtils { tokens.splice(inputIndex + 1); } - tokens.forEach((token) => { + tokens.forEach(token => { if (token.classList.contains('js-visual-token')) { const name = token.querySelector('.name'); const value = token.querySelector('.value'); @@ -194,8 +197,9 @@ export default class DropdownUtils { values.push(name.innerText); } } else if (token.classList.contains('input-token')) { - const { isLastVisualTokenValid } = - FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const { + isLastVisualTokenValid, + } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const input = FilteredSearchContainer.container.querySelector('.filtered-search'); const inputValue = input && input.value; @@ -209,9 +213,7 @@ export default class DropdownUtils { } }); - return values - .map(value => value.trim()) - .join(' '); + return values.map(value => value.trim()).join(' '); } static getSearchInput(filteredSearchInput) { @@ -227,7 +229,9 @@ export default class DropdownUtils { // Replace all spaces inside quote marks with underscores // (will continue to match entire string until an end quote is found if any) // This helps with matching the beginning & end of a token:key - inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_')); + inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => + str.replace(/\s/g, '_'), + ); // Get the right position for the word selected // Regex matches first space diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index fb4ae1d17dd..4eb67ff7649 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -87,10 +87,12 @@ export default class FilteredSearchDropdown { dispatchInputEvent() { // Propogate input change to FilteredSearchDropdownManager // so that it can determine which dropdowns to open - this.input.dispatchEvent(new CustomEvent('input', { - bubbles: true, - cancelable: true, - })); + this.input.dispatchEvent( + new CustomEvent('input', { + bubbles: true, + cancelable: true, + }), + ); } dispatchFormSubmitEvent() { @@ -114,7 +116,7 @@ export default class FilteredSearchDropdown { if (!data) return; - const results = data.map((o) => { + const results = data.map(o => { const updated = o; updated.droplab_hidden = false; return updated; diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index a09ad3e4758..e01dedbb57c 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -42,19 +42,21 @@ export default class FilteredSearchTokenKeys { } searchByKeyParam(keyParam) { - return this.tokenKeysWithAlternative.find((tokenKey) => { - let tokenKeyParam = tokenKey.key; + return ( + this.tokenKeysWithAlternative.find(tokenKey => { + let tokenKeyParam = tokenKey.key; - // Replace hyphen with underscore to compare keyParam with tokenKeyParam - // e.g. 'my-reaction' => 'my_reaction' - tokenKeyParam = tokenKeyParam.replace('-', '_'); + // Replace hyphen with underscore to compare keyParam with tokenKeyParam + // e.g. 'my-reaction' => 'my_reaction' + tokenKeyParam = tokenKeyParam.replace('-', '_'); - if (tokenKey.param) { - tokenKeyParam += `_${tokenKey.param}`; - } + if (tokenKey.param) { + tokenKeyParam += `_${tokenKey.param}`; + } - return keyParam === tokenKeyParam; - }) || null; + return keyParam === tokenKeyParam; + }) || null + ); } searchByConditionUrl(url) { @@ -62,8 +64,10 @@ export default class FilteredSearchTokenKeys { } searchByConditionKeyValue(key, value) { - return this.conditions - .find(condition => condition.tokenKey === key && condition.value === value) || null; + return ( + this.conditions.find(condition => condition.tokenKey === key && condition.value === value) || + null + ); } addExtraTokensForMergeRequests() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js index d75610f6d68..b5c4cb15aac 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js @@ -4,41 +4,48 @@ export default class FilteredSearchTokenizer { static processTokens(input, allowedKeys) { // Regex extracts `(token):(symbol)(value)` // Values that start with a double quote must end in a double quote (same for single) - const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); + const tokenRegex = new RegExp( + `(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, + 'g', + ); const tokens = []; const tokenIndexes = []; // stores key+value for simple search let lastToken = null; - const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { - let tokenValue = v1 || v2 || v3; - let tokenSymbol = symbol; - let tokenIndex = ''; - - if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { - tokenSymbol = tokenValue; - tokenValue = ''; - } - - tokenIndex = `${key}:${tokenValue}`; - - // Prevent adding duplicates - if (tokenIndexes.indexOf(tokenIndex) === -1) { - tokenIndexes.push(tokenIndex); - - tokens.push({ - key, - value: tokenValue || '', - symbol: tokenSymbol || '', - }); - } - - return ''; - }).replace(/\s{2,}/g, ' ').trim() || ''; + const searchToken = + input + .replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { + let tokenValue = v1 || v2 || v3; + let tokenSymbol = symbol; + let tokenIndex = ''; + + if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { + tokenSymbol = tokenValue; + tokenValue = ''; + } + + tokenIndex = `${key}:${tokenValue}`; + + // Prevent adding duplicates + if (tokenIndexes.indexOf(tokenIndex) === -1) { + tokenIndexes.push(tokenIndex); + + tokens.push({ + key, + value: tokenValue || '', + symbol: tokenSymbol || '', + }); + } + + return ''; + }) + .replace(/\s{2,}/g, ' ') + .trim() || ''; if (tokens.length > 0) { const last = tokens[tokens.length - 1]; const lastString = `${last.key}:${last.symbol}${last.value}`; - lastToken = input.lastIndexOf(lastString) === - input.length - lastString.length ? last : searchToken; + lastToken = + input.lastIndexOf(lastString) === input.length - lastString.length ? last : searchToken; } else { lastToken = searchToken; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 0854c1822fb..c23d4c484a5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -13,7 +13,10 @@ export default class FilteredSearchVisualTokens { return { lastVisualToken, - isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null), + isLastVisualTokenValid: + lastVisualToken === null || + lastVisualToken.className.indexOf('filtered-search-term') !== -1 || + (lastVisualToken && lastVisualToken.querySelector('.value') !== null), }; } @@ -33,7 +36,9 @@ export default class FilteredSearchVisualTokens { } static unselectTokens() { - const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected'); + const otherTokens = FilteredSearchContainer.container.querySelectorAll( + '.js-visual-token .selectable.selected', + ); [].forEach.call(otherTokens, t => t.classList.remove('selected')); } @@ -56,11 +61,7 @@ export default class FilteredSearchVisualTokens { } static createVisualTokenElementHTML(options = {}) { - const { - canEdit = true, - uppercaseTokenName = false, - capitalizeTokenValue = false, - } = options; + const { canEdit = true, uppercaseTokenName = false, capitalizeTokenValue = false } = options; return ` <div class="${canEdit ? 'selectable' : 'hidden'}" role="button"> @@ -115,15 +116,20 @@ export default class FilteredSearchVisualTokens { return AjaxCache.retrieve(labelsEndpoint) .then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint)) - .then((labels) => { - const matchingLabel = (labels || []).find(label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue); + .then(labels => { + const matchingLabel = (labels || []).find( + label => `~${DropdownUtils.getEscapedText(label.title)}` === tokenValue, + ); if (!matchingLabel) { return; } - FilteredSearchVisualTokens - .setTokenStyle(tokenValueContainer, matchingLabel.color, matchingLabel.text_color); + FilteredSearchVisualTokens.setTokenStyle( + tokenValueContainer, + matchingLabel.color, + matchingLabel.text_color, + ); }) .catch(() => new Flash('An error occurred while fetching label colors.')); } @@ -134,39 +140,43 @@ export default class FilteredSearchVisualTokens { } const username = tokenValue.replace(/^@/, ''); - return UsersCache.retrieve(username) - .then((user) => { - if (!user) { - return; - } - - /* eslint-disable no-param-reassign */ - tokenValueContainer.dataset.originalValue = tokenValue; - tokenValueElement.innerHTML = ` + return ( + UsersCache.retrieve(username) + .then(user => { + if (!user) { + return; + } + + /* eslint-disable no-param-reassign */ + tokenValueContainer.dataset.originalValue = tokenValue; + tokenValueElement.innerHTML = ` <img class="avatar s20" src="${user.avatar_url}" alt=""> ${_.escape(user.name)} `; - /* eslint-enable no-param-reassign */ - }) - // ignore error and leave username in the search bar - .catch(() => { }); + /* eslint-enable no-param-reassign */ + }) + // ignore error and leave username in the search bar + .catch(() => {}) + ); } static updateEmojiTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) { const container = tokenValueContainer; const element = tokenValueElement; - return import(/* webpackChunkName: 'emoji' */ '../emoji') - .then((Emoji) => { - if (!Emoji.isEmojiNameValid(tokenValue)) { - return; - } - - container.dataset.originalValue = tokenValue; - element.innerHTML = Emoji.glEmojiTag(tokenValue); - }) - // ignore error and leave emoji name in the search bar - .catch(() => { }); + return ( + import(/* webpackChunkName: 'emoji' */ '../emoji') + .then(Emoji => { + if (!Emoji.isEmojiNameValid(tokenValue)) { + return; + } + + container.dataset.originalValue = tokenValue; + element.innerHTML = Emoji.glEmojiTag(tokenValue); + }) + // ignore error and leave emoji name in the search bar + .catch(() => {}) + ); } static renderVisualTokenValue(parentElement, tokenName, tokenValue) { @@ -177,24 +187,23 @@ export default class FilteredSearchVisualTokens { const tokenType = tokenName.toLowerCase(); if (tokenType === 'label') { FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue); - } else if ((tokenType === 'author') || (tokenType === 'assignee')) { + } else if (tokenType === 'author' || tokenType === 'assignee') { FilteredSearchVisualTokens.updateUserTokenAppearance( - tokenValueContainer, tokenValueElement, tokenValue, + tokenValueContainer, + tokenValueElement, + tokenValue, ); } else if (tokenType === 'my-reaction') { FilteredSearchVisualTokens.updateEmojiTokenAppearance( - tokenValueContainer, tokenValueElement, tokenValue, + tokenValueContainer, + tokenValueElement, + tokenValue, ); } } static addVisualTokenElement(name, value, options = {}) { - const { - isSearchTerm = false, - canEdit, - uppercaseTokenName, - capitalizeTokenValue, - } = options; + const { isSearchTerm = false, canEdit, uppercaseTokenName, capitalizeTokenValue } = options; const li = document.createElement('li'); li.classList.add('js-visual-token'); li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); @@ -217,8 +226,10 @@ export default class FilteredSearchVisualTokens { } static addValueToPreviousVisualTokenElement(value) { - const { lastVisualToken, isLastVisualTokenValid } = - FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const { + lastVisualToken, + isLastVisualTokenValid, + } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) { const name = FilteredSearchVisualTokens.getLastTokenPartial(); @@ -228,13 +239,15 @@ export default class FilteredSearchVisualTokens { } } - static addFilterVisualToken(tokenName, tokenValue, { - canEdit, - uppercaseTokenName = false, - capitalizeTokenValue = false, - } = {}) { - const { lastVisualToken, isLastVisualTokenValid } - = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + static addFilterVisualToken( + tokenName, + tokenValue, + { canEdit, uppercaseTokenName = false, capitalizeTokenValue = false } = {}, + ) { + const { + lastVisualToken, + isLastVisualTokenValid, + } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const { addVisualTokenElement } = FilteredSearchVisualTokens; if (isLastVisualTokenValid) { @@ -308,8 +321,7 @@ export default class FilteredSearchVisualTokens { static tokenizeInput() { const input = FilteredSearchContainer.container.querySelector('.filtered-search'); - const { isLastVisualTokenValid } = - FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); if (input.value) { if (isLastVisualTokenValid) { @@ -375,8 +387,7 @@ export default class FilteredSearchVisualTokens { FilteredSearchVisualTokens.tokenizeInput(); if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) { - const { isLastVisualTokenValid } = - FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); + const { isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); if (!isLastVisualTokenValid) { const lastPartial = FilteredSearchVisualTokens.getLastTokenPartial(); diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js index cc7291c9f59..c4f0c41d3a8 100644 --- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -1,34 +1,39 @@ import FilteredSearchTokenKeys from './filtered_search_token_keys'; -export const tokenKeys = [{ - key: 'author', - type: 'string', - param: 'username', - symbol: '@', - icon: 'pencil', - tag: '@author', -}, { - key: 'assignee', - type: 'string', - param: 'username', - symbol: '@', - icon: 'user', - tag: '@assignee', -}, { - key: 'milestone', - type: 'string', - param: 'title', - symbol: '%', - icon: 'clock', - tag: '%milestone', -}, { - key: 'label', - type: 'array', - param: 'name[]', - symbol: '~', - icon: 'labels', - tag: '~label', -}]; +export const tokenKeys = [ + { + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + icon: 'pencil', + tag: '@author', + }, + { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + icon: 'user', + tag: '@assignee', + }, + { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + icon: 'clock', + tag: '%milestone', + }, + { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + icon: 'labels', + tag: '~label', + }, +]; if (gon.current_user_id) { // Appending tokenkeys only logged-in @@ -42,36 +47,47 @@ if (gon.current_user_id) { }); } -export const alternativeTokenKeys = [{ - key: 'label', - type: 'string', - param: 'name', - symbol: '~', -}]; +export const alternativeTokenKeys = [ + { + key: 'label', + type: 'string', + param: 'name', + symbol: '~', + }, +]; -export const conditions = [{ - url: 'assignee_id=0', - tokenKey: 'assignee', - value: 'none', -}, { - url: 'milestone_title=No+Milestone', - tokenKey: 'milestone', - value: 'none', -}, { - url: 'milestone_title=%23upcoming', - tokenKey: 'milestone', - value: 'upcoming', -}, { - url: 'milestone_title=%23started', - tokenKey: 'milestone', - value: 'started', -}, { - url: 'label_name[]=No+Label', - tokenKey: 'label', - value: 'none', -}]; +export const conditions = [ + { + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', + }, + { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', + }, + { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', + }, + { + url: 'milestone_title=%23started', + tokenKey: 'milestone', + value: 'started', + }, + { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', + }, +]; -const IssuableFilteredSearchTokenKeys = - new FilteredSearchTokenKeys(tokenKeys, alternativeTokenKeys, conditions); +const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys( + tokenKeys, + alternativeTokenKeys, + conditions, +); export default IssuableFilteredSearchTokenKeys; diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index c1efa9c86f4..6c8e77a7fe5 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -3,11 +3,7 @@ import RecentSearchesDropdownContent from './components/recent_searches_dropdown import eventHub from './event_hub'; class RecentSearchesRoot { - constructor( - recentSearchesStore, - recentSearchesService, - wrapperElement, - ) { + constructor(recentSearchesStore, recentSearchesService, wrapperElement) { this.store = recentSearchesStore; this.service = recentSearchesService; this.wrapperElement = wrapperElement; @@ -35,7 +31,9 @@ class RecentSearchesRoot { components: { RecentSearchesDropdownContent, }, - data() { return state; }, + data() { + return state; + }, template: ` <recent-searches-dropdown-content :items="recentSearches" @@ -57,7 +55,6 @@ class RecentSearchesRoot { this.vm.$destroy(); } } - } export default RecentSearchesRoot; diff --git a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js index aaa0c349d93..76d40bfdaf8 100644 --- a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js +++ b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js @@ -2,11 +2,14 @@ import _ from 'underscore'; class RecentSearchesStore { constructor(initialState = {}, allowedKeys) { - this.state = Object.assign({ - isLocalStorageAvailable: true, - recentSearches: [], - allowedKeys, - }, initialState); + this.state = Object.assign( + { + isLocalStorageAvailable: true, + recentSearches: [], + allowedKeys, + }, + initialState, + ); } addRecentSearch(newSearch) { diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue index a6a265eb3fd..14c223c61a4 100644 --- a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue +++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue @@ -1,10 +1,14 @@ <script> import _ from 'underscore'; import { mapActions } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub'; import frequentItemsMixin from './frequent_items_mixin'; export default { + components: { + Icon, + }, mixins: [frequentItemsMixin], data() { return { @@ -45,11 +49,10 @@ export default { type="search" class="form-control" /> - <i + <icon v-if="!searchQuery" - class="search-icon fa fa-fw fa-search" - aria-hidden="true" - > - </i> + name="search" + class="search-icon" + /> </div> </template> diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js index d519c033769..25a65b047f1 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js @@ -13,6 +13,7 @@ export default { }, [types.SET_SELECTED_TEMPLATE_TYPE](state, type) { state.selectedTemplateType = type; + state.templates = []; }, [types.SET_UPDATE_SUCCESS](state, success) { state.updateSuccess = success; diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index 06ec4546164..ffcbd7cf28c 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -26,14 +26,17 @@ export default class IssuableIndex { static resetIncomingEmailToken() { const $resetToken = $('.incoming-email-token-reset'); - $resetToken.on('click', (e) => { + $resetToken.on('click', e => { e.preventDefault(); $resetToken.text('resetting...'); - axios.put($resetToken.attr('href')) + axios + .put($resetToken.attr('href')) .then(({ data }) => { - $('#issuable_email').val(data.new_address).focus(); + $('#issuable_email') + .val(data.new_address) + .focus(); $resetToken.text('reset it'); }) diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 4b4e9aa48ab..94b78907d9a 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -28,7 +28,7 @@ export default class Issue { } // Listen to state changes in the Vue app - document.addEventListener('issuable_vue_app:change', (event) => { + document.addEventListener('issuable_vue_app:change', event => { this.updateTopState(event.detail.isClosed, event.detail.data); }); } @@ -55,7 +55,13 @@ export default class Issue { $(document).trigger('issuable:change', isClosed); this.toggleCloseReopenButton(isClosed); - let numProjectIssues = Number(projectIssuesCounter.first().text().trim().replace(/[^\d]/, '')); + let numProjectIssues = Number( + projectIssuesCounter + .first() + .text() + .trim() + .replace(/[^\d]/, ''), + ); numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; projectIssuesCounter.text(addDelimiter(numProjectIssues)); @@ -76,29 +82,34 @@ export default class Issue { initIssueBtnEventListeners() { const issueFailMessage = 'Unable to update this issue at this time.'; - return $(document).on('click', '.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen', (e) => { - var $button, shouldSubmit, url; - e.preventDefault(); - e.stopImmediatePropagation(); - $button = $(e.currentTarget); - shouldSubmit = $button.hasClass('btn-comment'); - if (shouldSubmit) { - Issue.submitNoteForm($button.closest('form')); - } - - this.disableCloseReopenButton($button); + return $(document).on( + 'click', + '.js-issuable-actions a.btn-close, .js-issuable-actions a.btn-reopen', + e => { + var $button, shouldSubmit, url; + e.preventDefault(); + e.stopImmediatePropagation(); + $button = $(e.currentTarget); + shouldSubmit = $button.hasClass('btn-comment'); + if (shouldSubmit) { + Issue.submitNoteForm($button.closest('form')); + } - url = $button.attr('href'); - return axios.put(url) - .then(({ data }) => { - const isClosed = $button.hasClass('btn-close'); - this.updateTopState(isClosed, data); - }) - .catch(() => flash(issueFailMessage)) - .then(() => { - this.disableCloseReopenButton($button, false); - }); - }); + this.disableCloseReopenButton($button); + + url = $button.attr('href'); + return axios + .put(url) + .then(({ data }) => { + const isClosed = $button.hasClass('btn-close'); + this.updateTopState(isClosed, data); + }) + .catch(() => flash(issueFailMessage)) + .then(() => { + this.disableCloseReopenButton($button, false); + }); + }, + ); } initCloseReopenReport() { @@ -124,7 +135,7 @@ export default class Issue { static submitNoteForm(form) { var noteText; - noteText = form.find("textarea.js-note-text").val(); + noteText = form.find('textarea.js-note-text').val(); if (noteText && noteText.trim().length > 0) { return form.submit(); } @@ -133,22 +144,26 @@ export default class Issue { static initMergeRequests() { var $container; $container = $('#merge-requests'); - return axios.get($container.data('url')) + return axios + .get($container.data('url')) .then(({ data }) => { if ('html' in data) { $container.html(data.html); } - }).catch(() => flash('Failed to load referenced merge requests')); + }) + .catch(() => flash('Failed to load referenced merge requests')); } static initRelatedBranches() { var $container; $container = $('#related-branches'); - return axios.get($container.data('url')) + return axios + .get($container.data('url')) .then(({ data }) => { if ('html' in data) { $container.html(data.html); } - }).catch(() => flash('Failed to load related branches')); + }) + .catch(() => flash('Failed to load related branches')); } } diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js deleted file mode 100644 index 854445bd2a4..00000000000 --- a/app/assets/javascripts/job.js +++ /dev/null @@ -1,190 +0,0 @@ -import $ from 'jquery'; -import _ from 'underscore'; -import { polyfillSticky } from './lib/utils/sticky'; -import axios from './lib/utils/axios_utils'; -import { visitUrl } from './lib/utils/url_utility'; -import bp from './breakpoints'; -import { numberToHumanSize } from './lib/utils/number_utils'; -import { setCiStatusFavicon } from './lib/utils/common_utils'; -import { isScrolledToBottom, scrollDown, scrollUp } from './lib/utils/scroll_utils'; -import LogOutputBehaviours from './lib/utils/logoutput_behaviours'; - -export default class Job extends LogOutputBehaviours { - constructor(options) { - super(); - this.timeout = null; - this.state = null; - this.fetchingStatusFavicon = false; - this.options = options || $('.js-build-options').data(); - - this.pagePath = this.options.pagePath; - this.buildStatus = this.options.buildStatus; - this.state = this.options.logState; - this.buildStage = this.options.buildStage; - this.$document = $(document); - this.$window = $(window); - this.logBytes = 0; - - this.$buildTrace = $('#build-trace'); - this.$buildRefreshAnimation = $('.js-build-refresh'); - this.$truncatedInfo = $('.js-truncated-info'); - this.$buildTraceOutput = $('.js-build-output'); - this.$topBar = $('.js-top-bar'); - - clearTimeout(this.timeout); - - this.initSidebar(); - this.sidebarOnResize(); - - this.$document - .off('click', '.js-sidebar-build-toggle') - .on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); - - this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); - - this.$window - .off('scroll') - .on('scroll', () => { - if (!isScrolledToBottom()) { - this.toggleScrollAnimation(false); - } else if (isScrolledToBottom() && !this.isLogComplete) { - this.toggleScrollAnimation(true); - } - this.scrollThrottled(); - }); - - this.$window - .off('resize.build') - .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); - - this.initAffixTopArea(); - - this.getBuildTrace(); - } - - initAffixTopArea() { - polyfillSticky(this.$topBar); - } - - scrollToBottom() { - scrollDown(); - this.hasBeenScrolled = true; - this.toggleScroll(); - } - - scrollToTop() { - scrollUp(); - this.hasBeenScrolled = true; - this.toggleScroll(); - } - - toggleScrollAnimation(toggle) { - this.$scrollBottomBtn.toggleClass('animate', toggle); - } - - initSidebar() { - this.$sidebar = $('.js-build-sidebar'); - } - - getBuildTrace() { - return axios.get(`${this.pagePath}/trace.json`, { - params: { state: this.state }, - }) - .then((res) => { - const log = res.data; - - if (!this.fetchingStatusFavicon) { - this.fetchingStatusFavicon = true; - - setCiStatusFavicon(`${this.pagePath}/status.json`) - .then(() => { - this.fetchingStatusFavicon = false; - }) - .catch(() => { - this.fetchingStatusFavicon = false; - }); - } - - if (log.state) { - this.state = log.state; - } - - this.isScrollInBottom = isScrolledToBottom(); - - if (log.append) { - this.$buildTraceOutput.append(log.html); - this.logBytes += log.size; - } else { - this.$buildTraceOutput.html(log.html); - this.logBytes = log.size; - } - - // if the incremental sum of logBytes we received is less than the total - // we need to show a message warning the user about that. - if (this.logBytes < log.total) { - // size is in bytes, we need to calculate KiB - const size = numberToHumanSize(this.logBytes); - $('.js-truncated-info-size').html(`${size}`); - this.$truncatedInfo.removeClass('hidden'); - } else { - this.$truncatedInfo.addClass('hidden'); - } - this.isLogComplete = log.complete; - - if (log.complete === false) { - this.timeout = setTimeout(() => { - this.getBuildTrace(); - }, 4000); - } else { - this.$buildRefreshAnimation.remove(); - this.toggleScrollAnimation(false); - } - - if (log.status !== this.buildStatus) { - visitUrl(this.pagePath); - } - }) - .catch(() => { - this.$buildRefreshAnimation.remove(); - }) - .then(() => { - if (this.isScrollInBottom) { - scrollDown(); - } - }) - .then(() => this.toggleScroll()); - } - // eslint-disable-next-line class-methods-use-this - shouldHideSidebarForViewport() { - const bootstrapBreakpoint = bp.getBreakpointSize(); - return bootstrapBreakpoint === 'xs'; - } - - toggleSidebar(shouldHide) { - const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; - const $toggleButton = $('.js-sidebar-build-toggle-header'); - - this.$sidebar - .toggleClass('right-sidebar-expanded', shouldShow) - .toggleClass('right-sidebar-collapsed', shouldHide); - - this.$topBar - .toggleClass('sidebar-expanded', shouldShow) - .toggleClass('sidebar-collapsed', shouldHide); - - if (this.$sidebar.hasClass('right-sidebar-expanded')) { - $toggleButton.addClass('hidden'); - } else { - $toggleButton.removeClass('hidden'); - } - } - - sidebarOnResize() { - this.toggleSidebar(this.shouldHideSidebarForViewport()); - } - - sidebarOnClick() { - if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); - } - -} diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index 4e8d3ad24cc..ba14aaeed2c 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -1,21 +1,32 @@ <script> - import { mapGetters, mapState } from 'vuex'; + import _ from 'underscore'; + import { mapGetters, mapState, mapActions } from 'vuex'; + import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; + import bp from '~/breakpoints'; import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import Callout from '~/vue_shared/components/callout.vue'; + import createStore from '../store'; import EmptyState from './empty_state.vue'; import EnvironmentsBlock from './environments_block.vue'; import ErasedBlock from './erased_block.vue'; + import Log from './job_log.vue'; + import LogTopBar from './job_log_controllers.vue'; import StuckBlock from './stuck_block.vue'; + import Sidebar from './sidebar.vue'; export default { name: 'JobPageApp', + store: createStore(), components: { CiHeader, Callout, EmptyState, EnvironmentsBlock, ErasedBlock, + Log, + LogTopBar, StuckBlock, + Sidebar, }, props: { runnerSettingsUrl: { @@ -23,9 +34,43 @@ required: false, default: null, }, + runnerHelpUrl: { + type: String, + required: false, + default: null, + }, + endpoint: { + type: String, + required: true, + }, + terminalPath: { + type: String, + required: false, + default: null, + }, + pagePath: { + type: String, + required: true, + }, + logState: { + type: String, + required: true, + }, }, computed: { - ...mapState(['isLoading', 'job']), + ...mapState([ + 'isLoading', + 'job', + 'isSidebarOpen', + 'trace', + 'isTraceComplete', + 'traceSize', + 'isTraceSizeVisible', + 'isScrollBottomDisabled', + 'isScrollTopDisabled', + 'isScrolledToBottomBeforeReceivingTrace', + 'hasError', + ]), ...mapGetters([ 'headerActions', 'headerTime', @@ -35,7 +80,83 @@ 'isJobStuck', 'hasTrace', 'emptyStateIllustration', + 'isScrollingDown', + 'emptyStateAction', ]), + + shouldRenderContent() { + return !this.isLoading && !this.hasError; + } + }, + watch: { + // Once the job log is loaded, + // fetch the stages for the dropdown on the sidebar + job(newVal, oldVal) { + if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { + this.fetchStages(); + } + }, + }, + created() { + this.throttled = _.throttle(this.toggleScrollButtons, 100); + + this.setJobEndpoint(this.endpoint); + this.setTraceOptions({ + logState: this.logState, + pagePath: this.pagePath, + }); + + this.fetchJob(); + this.fetchTrace(); + + window.addEventListener('resize', this.onResize); + window.addEventListener('scroll', this.updateScroll); + }, + + mounted() { + this.updateSidebar(); + }, + + destroyed() { + window.removeEventListener('resize', this.onResize); + window.removeEventListener('scroll', this.updateScroll); + }, + + methods: { + ...mapActions([ + 'setJobEndpoint', + 'setTraceOptions', + 'fetchJob', + 'fetchStages', + 'hideSidebar', + 'showSidebar', + 'toggleSidebar', + 'fetchTrace', + 'scrollBottom', + 'scrollTop', + 'toggleScrollButtons', + 'toggleScrollAnimation', + ]), + onResize() { + this.updateSidebar(); + this.updateScroll(); + }, + updateSidebar() { + if (bp.getBreakpointSize() === 'xs') { + this.hideSidebar(); + } else if (!this.isSidebarOpen) { + this.showSidebar(); + } + }, + updateScroll() { + if (!isScrolledToBottom()) { + this.toggleScrollAnimation(false); + } else if (this.isScrollingDown) { + this.toggleScrollAnimation(true); + } + + this.throttled(); + }, }, }; </script> @@ -44,71 +165,107 @@ <gl-loading-icon v-if="isLoading" :size="2" - class="prepend-top-20" + class="js-job-loading qa-loading-animation prepend-top-20" /> - <template v-else> - <!-- Header Section --> - <header> - <div class="js-build-header build-header top-area"> - <ci-header - :status="job.status" - :item-id="job.id" - :time="headerTime" - :user="job.user" - :actions="headerActions" - :has-sidebar-button="true" - :should-render-triggered-label="shouldRenderTriggeredLabel" - :item-name="__('Job')" + <template v-else-if="shouldRenderContent"> + <div class="js-job-content build-page"> + <!-- Header Section --> + <header> + <div class="js-build-header build-header top-area"> + <ci-header + :status="job.status" + :item-id="job.id" + :time="headerTime" + :user="job.user" + :actions="headerActions" + :has-sidebar-button="true" + :should-render-triggered-label="shouldRenderTriggeredLabel" + :item-name="__('Job')" + @clickedSidebarButton="toggleSidebar" + /> + </div> + + <callout + v-if="shouldRenderCalloutMessage" + :message="job.callout_message" + /> + </header> + <!-- EO Header Section --> + + <!-- Body Section --> + <stuck-block + v-if="isJobStuck" + class="js-job-stuck" + :has-no-runners-for-project="job.runners.available" + :tags="job.tags" + :runners-path="runnerSettingsUrl" + /> + + <environments-block + v-if="hasEnvironment" + class="js-job-environment" + :deployment-status="job.deployment_status" + :icon-status="job.status" + /> + + <erased-block + v-if="job.erased_at" + class="js-job-erased-block" + :user="job.erased_by" + :erased-at="job.erased_at" + /> + + <!--job log --> + <div + v-if="hasTrace" + class="build-trace-container prepend-top-default"> + <log-top-bar + :class="{ + 'sidebar-expanded': isSidebarOpen, + 'sidebar-collapsed': !isSidebarOpen + }" + :erase-path="job.erase_path" + :size="traceSize" + :raw-path="job.raw_path" + :is-scroll-bottom-disabled="isScrollBottomDisabled" + :is-scroll-top-disabled="isScrollTopDisabled" + :is-trace-size-visible="isTraceSizeVisible" + :is-scrolling-down="isScrollingDown" + @scrollJobLogTop="scrollTop" + @scrollJobLogBottom="scrollBottom" + /> + <log + :trace="trace" + :is-complete="isTraceComplete" /> </div> + <!-- EO job log --> - <callout - v-if="shouldRenderCalloutMessage" - :message="job.callout_message" + <!--empty state --> + <empty-state + v-if="!hasTrace" + class="js-job-empty-state" + :illustration-path="emptyStateIllustration.image" + :illustration-size-class="emptyStateIllustration.size" + :title="emptyStateIllustration.title" + :content="emptyStateIllustration.content" + :action="emptyStateAction" /> - </header> - <!-- EO Header Section --> - - <!-- Body Section --> - <stuck-block - v-if="isJobStuck" - class="js-job-stuck" - :has-no-runners-for-project="job.runners.available" - :tags="job.tags" - :runners-path="runnerSettingsUrl" - /> - - <environments-block - v-if="hasEnvironment" - class="js-job-environment" - :deployment-status="job.deployment_status" - :icon-status="job.status" - /> - - <erased-block - v-if="job.erased_at" - class="js-job-erased-block" - :user="job.erased_by" - :erased-at="job.erased_at" - /> - - <!--job log --> - <!-- EO job log --> - - <!--empty state --> - <empty-state - v-if="!hasTrace" - class="js-job-empty-state" - :illustration-path="emptyStateIllustration.image" - :illustration-size-class="emptyStateIllustration.size" - :title="emptyStateIllustration.title" - :content="emptyStateIllustration.content" - :action="job.status.action" - /> <!-- EO empty state --> <!-- EO Body Section --> + </div> </template> + + <sidebar + v-if="shouldRenderContent" + class="js-job-sidebar" + :class="{ + 'right-sidebar-expanded': isSidebarOpen, + 'right-sidebar-collapsed': !isSidebarOpen + }" + :runner-help-url="runnerHelpUrl" + /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue index 9d78d89239a..ffa6ada3e28 100644 --- a/app/assets/javascripts/jobs/components/job_log.vue +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -1,20 +1,48 @@ <script> -export default { - name: 'JobLog', - props: { - trace: { - type: String, - required: true, + import { mapState, mapActions } from 'vuex'; + + export default { + name: 'JobLog', + props: { + trace: { + type: String, + required: true, + }, + isComplete: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapState(['isScrolledToBottomBeforeReceivingTrace']), + }, + updated() { + this.$nextTick(() => this.handleScrollDown()); + }, + mounted() { + this.$nextTick(() => this.handleScrollDown()); }, - isComplete: { - type: Boolean, - required: true, + methods: { + ...mapActions(['scrollBottom']), + /** + * The job log is sent in HTML, which means we need to use `v-html` to render it + * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated + * in this case because it runs before `v-html` has finished running, since there's no + * Vue binding. + * In order to scroll the page down after `v-html` has finished, we need to use setTimeout + */ + handleScrollDown() { + if (this.isScrolledToBottomBeforeReceivingTrace) { + setTimeout(() => { + this.scrollBottom(); + }, 0); + } + }, }, - }, -}; + }; </script> <template> - <pre class="build-trace"> + <pre class="js-build-trace build-trace qa-build-trace"> <code class="bash" v-html="trace" @@ -22,7 +50,7 @@ export default { </code> <div - v-if="isComplete" + v-if="!isComplete" class="js-log-animation build-loader-animation" > <div class="dot"></div> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index cc885ea8e1b..94ab1b16c84 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -4,6 +4,7 @@ import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { sprintf } from '~/locale'; +import scrollDown from '../svg/scroll_down.svg'; export default { components: { @@ -12,6 +13,7 @@ export default { directives: { tooltip, }, + scrollDown, props: { erasePath: { type: String, @@ -65,7 +67,7 @@ export default { }; </script> <template> - <div class="top-bar affix js-top-bar"> + <div class="top-bar affix"> <!-- truncate information --> <div class="js-truncated-info truncated-info d-none d-sm-block float-left"> <template v-if="isTraceSizeVisible"> @@ -100,7 +102,7 @@ export default { v-tooltip :title="s__('Job|Erase job log')" :href="erasePath" - data-confirm="__('Are you sure you want to erase this build?')" + :data-confirm="__('Are you sure you want to erase this build?')" class="js-erase-link controllers-buttons" data-container="body" data-method="post" @@ -138,8 +140,8 @@ export default { class="js-scroll-bottom btn-scroll btn-transparent btn-blank" :class="{ animate: isScrollingDown }" @click="handleScrollToBottom" + v-html="$options.scrollDown" > - <icon name="scroll_down"/> </button> </div> <!-- eo scroll buttons --> diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index 8f3c6aced23..906769ee6a2 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -29,14 +29,9 @@ export default { required: false, default: '', }, - terminalPath: { - type: String, - required: false, - default: null, - }, }, computed: { - ...mapState(['job', 'isLoading', 'stages', 'jobs', 'selectedStage']), + ...mapState(['job', 'stages', 'jobs', 'selectedStage']), coverage() { return `${this.job.coverage}%`; }, @@ -64,10 +59,10 @@ export default { return ''; } - let t = this.job.metadata.timeout_human_readable; - if (this.job.metadata.timeout_source !== '') { - t += ` (from ${this.job.metadata.timeout_source})`; - } + let t = this.job.metadata.timeout_human_readable; + if (this.job.metadata.timeout_source !== '') { + t += ` (from ${this.job.metadata.timeout_source})`; + } return t; }, @@ -100,196 +95,190 @@ export default { ); }, commit() { - return this.job.pipeline.commit || {}; + return this.job.pipeline && this.job.pipeline.commit ? this.job.pipeline.commit : {}; }, }, methods: { - ...mapActions(['fetchJobsForStage']), + ...mapActions(['fetchJobsForStage', 'toggleSidebar']), }, }; </script> <template> <aside - class="js-build-sidebar right-sidebar right-sidebar-expanded build-sidebar" + class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix" > <div class="sidebar-container"> <div class="blocks-container"> - <template v-if="!isLoading"> - <div class="block"> - <strong class="inline prepend-top-8"> - {{ job.name }} - </strong> - <a - v-if="job.retry_path" - :class="retryButtonClass" - :href="job.retry_path" - data-method="post" - rel="nofollow" - > - {{ __('Retry') }} - </a> - <a - v-if="terminalPath" - :href="terminalPath" - class="js-terminal-link pull-right btn btn-primary - btn-inverted visible-md-block visible-lg-block" - target="_blank" - > - {{ __('Debug') }} - <icon name="external-link" /> - </a> - <button - :aria-label="__('Toggle Sidebar')" - type="button" - class="btn btn-blank gutter-toggle - float-right d-block d-md-none js-sidebar-build-toggle" - > - <i - aria-hidden="true" - data-hidden="true" - class="fa fa-angle-double-right" - ></i> - </button> - </div> - <div - v-if="job.retry_path || job.new_issue_path" - class="block retry-link" + <div class="block"> + <strong class="inline prepend-top-8"> + {{ job.name }} + </strong> + <a + v-if="job.retry_path" + :class="retryButtonClass" + :href="job.retry_path" + data-method="post" + rel="nofollow" > - <a - v-if="job.new_issue_path" - :href="job.new_issue_path" - class="js-new-issue btn btn-success btn-inverted" - > - {{ __('New issue') }} + {{ __('Retry') }} + </a> + <a + v-if="job.terminal_path" + :href="job.terminal_path" + class="js-terminal-link pull-right btn btn-primary + btn-inverted visible-md-block visible-lg-block" + target="_blank" + > + {{ __('Debug') }} + <icon name="external-link" /> + </a> + <button + :aria-label="__('Toggle Sidebar')" + type="button" + class="btn btn-blank gutter-toggle + float-right d-block d-md-none js-sidebar-build-toggle" + @click="toggleSidebar" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-angle-double-right" + ></i> + </button> + </div> + <div + v-if="job.retry_path || job.new_issue_path" + class="block retry-link" + > + <a + v-if="job.new_issue_path" + :href="job.new_issue_path" + class="js-new-issue btn btn-success btn-inverted" + > + {{ __('New issue') }} + </a> + <a + v-if="job.retry_path" + :href="job.retry_path" + class="js-retry-job btn btn-inverted-secondary" + data-method="post" + rel="nofollow" + > + {{ __('Retry') }} + </a> + </div> + <div :class="{ block : renderBlock }"> + <p + v-if="job.merge_request" + class="build-detail-row js-job-mr" + > + <span class="build-light-text"> + {{ __('Merge Request:') }} + </span> + <a :href="job.merge_request.path"> + !{{ job.merge_request.iid }} </a> + </p> + + <detail-row + v-if="job.duration" + :value="duration" + class="js-job-duration" + title="Duration" + /> + <detail-row + v-if="job.finished_at" + :value="timeFormated(job.finished_at)" + class="js-job-finished" + title="Finished" + /> + <detail-row + v-if="job.erased_at" + :value="timeFormated(job.erased_at)" + class="js-job-erased" + title="Erased" + /> + <detail-row + v-if="job.queued" + :value="queued" + class="js-job-queued" + title="Queued" + /> + <detail-row + v-if="hasTimeout" + :help-url="runnerHelpUrl" + :value="timeout" + class="js-job-timeout" + title="Timeout" + /> + <detail-row + v-if="job.runner" + :value="runnerId" + class="js-job-runner" + title="Runner" + /> + <detail-row + v-if="job.coverage" + :value="coverage" + class="js-job-coverage" + title="Coverage" + /> + <p + v-if="job.tags.length" + class="build-detail-row js-job-tags" + > + <span class="build-light-text"> + {{ __('Tags:') }} + </span> + <span + v-for="(tag, i) in job.tags" + :key="i" + class="label label-primary"> + {{ tag }} + </span> + </p> + + <div + v-if="job.cancel_path" + class="btn-group prepend-top-5" + role="group"> <a - v-if="job.retry_path" - :href="job.retry_path" - class="js-retry-job btn btn-inverted-secondary" + :href="job.cancel_path" + class="js-cancel-job btn btn-sm btn-default" data-method="post" rel="nofollow" > - {{ __('Retry') }} + {{ __('Cancel') }} </a> </div> - <div :class="{ block : renderBlock }"> - <p - v-if="job.merge_request" - class="build-detail-row js-job-mr" - > - <span class="build-light-text"> - {{ __('Merge Request:') }} - </span> - <a :href="job.merge_request.path"> - !{{ job.merge_request.iid }} - </a> - </p> - - <detail-row - v-if="job.duration" - :value="duration" - class="js-job-duration" - title="Duration" - /> - <detail-row - v-if="job.finished_at" - :value="timeFormated(job.finished_at)" - class="js-job-finished" - title="Finished" - /> - <detail-row - v-if="job.erased_at" - :value="timeFormated(job.erased_at)" - class="js-job-erased" - title="Erased" - /> - <detail-row - v-if="job.queued" - :value="queued" - class="js-job-queued" - title="Queued" - /> - <detail-row - v-if="hasTimeout" - :help-url="runnerHelpUrl" - :value="timeout" - class="js-job-timeout" - title="Timeout" - /> - <detail-row - v-if="job.runner" - :value="runnerId" - class="js-job-runner" - title="Runner" - /> - <detail-row - v-if="job.coverage" - :value="coverage" - class="js-job-coverage" - title="Coverage" - /> - <p - v-if="job.tags.length" - class="build-detail-row js-job-tags" - > - <span class="build-light-text"> - {{ __('Tags:') }} - </span> - <span - v-for="(tag, i) in job.tags" - :key="i" - class="label label-primary"> - {{ tag }} - </span> - </p> - - <div - v-if="job.cancel_path" - class="btn-group prepend-top-5" - role="group"> - <a - :href="job.cancel_path" - class="js-cancel-job btn btn-sm btn-default" - data-method="post" - rel="nofollow" - > - {{ __('Cancel') }} - </a> - </div> - </div> - <artifacts-block - v-if="hasArtifact" - :artifact="job.artifact" - /> - <trigger-block - v-if="hasTriggers" - :trigger="job.trigger" - /> - <commit-block - :is-last-block="hasStages" - :commit="commit" - :merge-request="job.merge_request" - /> + </div> - <stages-dropdown - :stages="stages" - :pipeline="job.pipeline" - :selected-stage="selectedStage" - @requestSidebarStageDropdown="fetchJobsForStage" - /> + <artifacts-block + v-if="hasArtifact" + :artifact="job.artifact" + /> + <trigger-block + v-if="hasTriggers" + :trigger="job.trigger" + /> + <commit-block + :is-last-block="hasStages" + :commit="commit" + :merge-request="job.merge_request" + /> - </template> - <gl-loading-icon - v-else - :size="2" - class="prepend-top-10" + <stages-dropdown + :stages="stages" + :pipeline="job.pipeline" + :selected-stage="selectedStage" + @requestSidebarStageDropdown="fetchJobsForStage" /> </div> <jobs-container - v-if="!isLoading && jobs.length" + v-if="jobs.length" :jobs="jobs" :job-id="job.id" /> diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js new file mode 100644 index 00000000000..ccd096a1da5 --- /dev/null +++ b/app/assets/javascripts/jobs/index.js @@ -0,0 +1,26 @@ +import Vue from 'vue'; +import JobApp from './components/job_app.vue'; + +export default () => { + const element = document.getElementById('js-job-vue-app'); + + return new Vue({ + el: element, + components: { + JobApp, + }, + render(createElement) { + return createElement('job-app', { + props: { + runnerHelpUrl: element.dataset.runnerHelpUrl, + runnerSettingsUrl: element.dataset.runnerSettingsUrl, + endpoint: element.dataset.endpoint, + pagePath: element.dataset.buildOptionsPagePath, + logState: element.dataset.buildOptionsLogState, + buildStatus: element.dataset.buildOptionsBuildStatus, + }, + }); + }, + }); +}; + diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js deleted file mode 100644 index 15cd79b1c50..00000000000 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ /dev/null @@ -1,76 +0,0 @@ -import _ from 'underscore'; -import { mapState, mapActions } from 'vuex'; -import Vue from 'vue'; -import Job from '../job'; -import JobApp from './components/job_app.vue'; -import Sidebar from './components/sidebar.vue'; -import createStore from './store'; - -export default () => { - const { dataset } = document.getElementById('js-job-details-vue'); - - - - const store = createStore(); - store.dispatch('setJobEndpoint', dataset.endpoint); - - store.dispatch('fetchJob'); - - // Header - // eslint-disable-next-line no-new - new Vue({ - el: '#js-build-header-vue', - components: { - JobApp, - }, - store, - computed: { - ...mapState(['job', 'isLoading']), - }, - render(createElement) { - return createElement('job-app', { - props: { - isLoading: this.isLoading, - job: this.job, - runnerSettingsUrl: dataset.runnerSettingsUrl, - }, - }); - }, - }); - - // Sidebar information block - const detailsBlockElement = document.getElementById('js-details-block-vue'); - const detailsBlockDataset = detailsBlockElement.dataset; - // eslint-disable-next-line - new Vue({ - el: detailsBlockElement, - components: { - Sidebar, - }, - computed: { - ...mapState(['job']), - }, - watch: { - job(newVal, oldVal) { - if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { - this.fetchStages(); - } - }, - }, - methods: { - ...mapActions(['fetchStages']), - }, - store, - render(createElement) { - return createElement('sidebar', { - props: { - runnerHelpUrl: dataset.runnerHelpUrl, - terminalPath: detailsBlockDataset.terminalPath, - }, - }); - }, - }); - - // eslint-disable-next-line no-new - new Job(); -}; diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js index d0040161dc3..54ed217572a 100644 --- a/app/assets/javascripts/jobs/store/actions.js +++ b/app/assets/javascripts/jobs/store/actions.js @@ -1,17 +1,32 @@ import Visibility from 'visibilityjs'; import * as types from './mutation_types'; -import axios from '../../lib/utils/axios_utils'; -import Poll from '../../lib/utils/poll'; -import { setCiStatusFavicon } from '../../lib/utils/common_utils'; -import flash from '../../flash'; -import { __ } from '../../locale'; +import axios from '~/lib/utils/axios_utils'; +import Poll from '~/lib/utils/poll'; +import { setFaviconOverlay, resetFavicon } from '~/lib/utils/common_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; +import { + canScroll, + isScrolledToBottom, + isScrolledToTop, + isScrolledToMiddle, + scrollDown, + scrollUp, +} from '~/lib/utils/scroll_utils'; export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint); -export const setTraceEndpoint = ({ commit }, endpoint) => - commit(types.SET_TRACE_ENDPOINT, endpoint); -export const setStagesEndpoint = ({ commit }, endpoint) => - commit(types.SET_STAGES_ENDPOINT, endpoint); -export const setJobsEndpoint = ({ commit }, endpoint) => commit(types.SET_JOBS_ENDPOINT, endpoint); +export const setTraceOptions = ({ commit }, options) => commit(types.SET_TRACE_OPTIONS, options); + +export const hideSidebar = ({ commit }) => commit(types.HIDE_SIDEBAR); +export const showSidebar = ({ commit }) => commit(types.SHOW_SIDEBAR); + +export const toggleSidebar = ({ dispatch, state }) => { + if (state.isSidebarOpen) { + dispatch('hideSidebar'); + } else { + dispatch('showSidebar'); + } +}; let eTagPoll; @@ -62,41 +77,84 @@ export const fetchJob = ({ state, dispatch }) => { }); }; -export const receiveJobSuccess = ({ commit }, data) => { +export const receiveJobSuccess = ({ commit }, data = {}) => { commit(types.RECEIVE_JOB_SUCCESS, data); + + if (data.favicon) { + setFaviconOverlay(data.favicon); + } else { + resetFavicon(); + } }; export const receiveJobError = ({ commit }) => { commit(types.RECEIVE_JOB_ERROR); flash(__('An error occurred while fetching the job.')); + resetFavicon(); }; /** * Job's Trace */ -export const scrollTop = ({ commit }) => { - commit(types.SCROLL_TO_TOP); - window.scrollTo({ top: 0 }); +export const scrollTop = ({ dispatch }) => { + scrollUp(); + dispatch('toggleScrollButtons'); }; -export const scrollBottom = ({ commit }) => { - commit(types.SCROLL_TO_BOTTOM); - window.scrollTo({ top: document.height }); +export const scrollBottom = ({ dispatch }) => { + scrollDown(); + dispatch('toggleScrollButtons'); +}; + +/** + * Responsible for toggling the disabled state of the scroll buttons + */ +export const toggleScrollButtons = ({ dispatch }) => { + if (canScroll()) { + if (isScrolledToMiddle()) { + dispatch('enableScrollTop'); + dispatch('enableScrollBottom'); + } else if (isScrolledToTop()) { + dispatch('disableScrollTop'); + dispatch('enableScrollBottom'); + } else if (isScrolledToBottom()) { + dispatch('disableScrollBottom'); + dispatch('enableScrollTop'); + } + } else { + dispatch('disableScrollBottom'); + dispatch('disableScrollTop'); + } +}; + +export const disableScrollBottom = ({ commit }) => commit(types.DISABLE_SCROLL_BOTTOM); +export const disableScrollTop = ({ commit }) => commit(types.DISABLE_SCROLL_TOP); +export const enableScrollBottom = ({ commit }) => commit(types.ENABLE_SCROLL_BOTTOM); +export const enableScrollTop = ({ commit }) => commit(types.ENABLE_SCROLL_TOP); + +/** + * While the automatic scroll down is active, + * we show the scroll down button with an animation + */ +export const toggleScrollAnimation = ({ commit }, toggle) => + commit(types.TOGGLE_SCROLL_ANIMATION, toggle); + +/** + * Responsible to handle automatic scroll + */ +export const toggleScrollisInBottom = ({ commit }, toggle) => { + commit(types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE, toggle); }; export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE); let traceTimeout; -export const fetchTrace = ({ dispatch, state }) => { - dispatch('requestTrace'); - +export const fetchTrace = ({ dispatch, state }) => axios .get(`${state.traceEndpoint}/trace.json`, { params: { state: state.traceState }, }) .then(({ data }) => { - if (!state.fetchingStatusFavicon) { - dispatch('fetchFavicon'); - } + dispatch('toggleScrollisInBottom', isScrolledToBottom()); dispatch('receiveTraceSuccess', data); if (!data.complete) { @@ -108,7 +166,7 @@ export const fetchTrace = ({ dispatch, state }) => { } }) .catch(() => dispatch('receiveTraceError')); -}; + export const stopPollingTrace = ({ commit }) => { commit(types.STOP_POLLING_TRACE); clearTimeout(traceTimeout); @@ -120,17 +178,6 @@ export const receiveTraceError = ({ commit }) => { flash(__('An error occurred while fetching the job log.')); }; -export const fetchFavicon = ({ state, dispatch }) => { - dispatch('requestStatusFavicon'); - setCiStatusFavicon(`${state.pagePath}/status.json`) - .then(() => dispatch('receiveStatusFaviconSuccess')) - .catch(() => dispatch('requestStatusFaviconError')); -}; -export const requestStatusFavicon = ({ commit }) => commit(types.REQUEST_STATUS_FAVICON); -export const receiveStatusFaviconSuccess = ({ commit }) => - commit(types.RECEIVE_STATUS_FAVICON_SUCCESS); -export const requestStatusFaviconError = ({ commit }) => commit(types.RECEIVE_STATUS_FAVICON_ERROR); - /** * Stages dropdown on sidebar */ diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 9f4f372e3d2..4ce395a9106 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import { __ } from '~/locale'; +import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; export const headerActions = state => { if (state.job.new_issue_path) { @@ -34,11 +35,12 @@ export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); * Used to check if it should render the job log or the empty state * @returns {Boolean} */ -export const hasTrace = state => state.job.has_trace || state.job.status.group === 'running'; +export const hasTrace = state => state.job.has_trace || (!_.isEmpty(state.job.status) && state.job.status.group === 'running'); export const emptyStateIllustration = state => (state.job && state.job.status && state.job.status.illustration) || {}; +export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {}; /** * When the job is pending and there are no available runners * we need to render the stuck block; @@ -46,8 +48,10 @@ export const emptyStateIllustration = state => * @returns {Boolean} */ export const isJobStuck = state => - state.job.status.group === 'pending' && + (!_.isEmpty(state.job.status) && state.job.status.group === 'pending') && (!_.isEmpty(state.job.runners) && state.job.runners.available === false); +export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js index e66e1d4f116..fd098f13e90 100644 --- a/app/assets/javascripts/jobs/store/mutation_types.js +++ b/app/assets/javascripts/jobs/store/mutation_types.js @@ -1,10 +1,19 @@ export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT'; -export const SET_TRACE_ENDPOINT = 'SET_TRACE_ENDPOINT'; -export const SET_STAGES_ENDPOINT = 'SET_STAGES_ENDPOINT'; -export const SET_JOBS_ENDPOINT = 'SET_JOBS_ENDPOINT'; +export const SET_TRACE_OPTIONS = 'SET_TRACE_OPTIONS'; + +export const HIDE_SIDEBAR = 'HIDE_SIDEBAR'; +export const SHOW_SIDEBAR = 'SHOW_SIDEBAR'; export const SCROLL_TO_TOP = 'SCROLL_TO_TOP'; export const SCROLL_TO_BOTTOM = 'SCROLL_TO_BOTTOM'; +export const DISABLE_SCROLL_BOTTOM = 'DISABLE_SCROLL_BOTTOM'; +export const DISABLE_SCROLL_TOP = 'DISABLE_SCROLL_TOP'; +export const ENABLE_SCROLL_BOTTOM = 'ENABLE_SCROLL_BOTTOM'; +export const ENABLE_SCROLL_TOP = 'ENABLE_SCROLL_TOP'; +// TODO +export const TOGGLE_SCROLL_ANIMATION = 'TOGGLE_SCROLL_ANIMATION'; + +export const TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE = 'TOGGLE_IS_SCROLL_IN_BOTTOM'; export const REQUEST_JOB = 'REQUEST_JOB'; export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS'; @@ -15,10 +24,6 @@ export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE'; export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS'; export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR'; -export const REQUEST_STATUS_FAVICON = 'REQUEST_STATUS_FAVICON'; -export const RECEIVE_STATUS_FAVICON_SUCCESS = 'RECEIVE_STATUS_FAVICON_SUCCESS'; -export const RECEIVE_STATUS_FAVICON_ERROR = 'RECEIVE_STATUS_FAVICON_ERROR'; - export const REQUEST_STAGES = 'REQUEST_STAGES'; export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS'; export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR'; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index f00e06e1a6c..4195d787f12 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -4,14 +4,17 @@ export default { [types.SET_JOB_ENDPOINT](state, endpoint) { state.jobEndpoint = endpoint; }, - [types.REQUEST_STATUS_FAVICON](state) { - state.fetchingStatusFavicon = true; + + [types.SET_TRACE_OPTIONS](state, options = {}) { + state.traceEndpoint = options.pagePath; + state.traceState = options.logState; }, - [types.RECEIVE_STATUS_FAVICON_SUCCESS](state) { - state.fetchingStatusFavicon = false; + + [types.HIDE_SIDEBAR](state) { + state.isSidebarOpen = false; }, - [types.RECEIVE_STATUS_FAVICON_ERROR](state) { - state.fetchingStatusFavicon = false; + [types.SHOW_SIDEBAR](state) { + state.isSidebarOpen = true; }, [types.RECEIVE_TRACE_SUCCESS](state, log) { @@ -23,8 +26,12 @@ export default { state.trace += log.html; state.traceSize += log.size; } else { - state.trace = log.html; - state.traceSize = log.size; + // When the job still does not have a trace + // the trace response will not have a defined + // html or size. We keep the old value otherwise these + // will be set to `undefined` + state.trace = log.html || state.trace; + state.traceSize = log.size || state.traceSize; } if (state.traceSize < log.total) { @@ -33,25 +40,29 @@ export default { state.isTraceSizeVisible = false; } - state.isTraceComplete = log.complete; - state.hasTraceError = false; + state.isTraceComplete = log.complete || state.isTraceComplete; }, + + /** + * Will remove loading animation + */ [types.STOP_POLLING_TRACE](state) { state.isTraceComplete = true; }, - // todo_fl: check this. + + /** + * Will remove loading animation + */ [types.RECEIVE_TRACE_ERROR](state) { - state.isLoadingTrace = false; state.isTraceComplete = true; - state.hasTraceError = true; }, [types.REQUEST_JOB](state) { state.isLoading = true; }, [types.RECEIVE_JOB_SUCCESS](state, job) { - state.isLoading = false; state.hasError = false; + state.isLoading = false; state.job = job; /** @@ -66,17 +77,28 @@ export default { }, [types.RECEIVE_JOB_ERROR](state) { state.isLoading = false; - state.hasError = true; state.job = {}; + state.hasError = true; }, - [types.SCROLL_TO_TOP](state) { - state.isTraceScrolledToBottom = false; - state.hasBeenScrolled = true; + [types.ENABLE_SCROLL_TOP](state) { + state.isScrollTopDisabled = false; + }, + [types.DISABLE_SCROLL_TOP](state) { + state.isScrollTopDisabled = true; + }, + [types.ENABLE_SCROLL_BOTTOM](state) { + state.isScrollBottomDisabled = false; }, - [types.SCROLL_TO_BOTTOM](state) { - state.isTraceScrolledToBottom = true; - state.hasBeenScrolled = true; + [types.DISABLE_SCROLL_BOTTOM](state) { + state.isScrollBottomDisabled = true; + }, + [types.TOGGLE_SCROLL_ANIMATION](state, toggle) { + state.isScrollingDown = toggle; + }, + + [types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_TRACE](state, toggle) { + state.isScrolledToBottomBeforeReceivingTrace = toggle; }, [types.REQUEST_STAGES](state) { diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index afbc959bb71..0eb269ca38f 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -4,36 +4,29 @@ export default () => ({ jobEndpoint: null, traceEndpoint: null, - // dropdown options - stagesEndpoint: null, - // list of jobs on sidebard - stageJobsEndpoint: null, + // sidebar + isSidebarOpen: true, - // job log isLoading: false, hasError: false, job: {}, - // trace - isLoadingTrace: false, - hasTraceError: false, + // scroll buttons state + isScrollBottomDisabled: true, + isScrollTopDisabled: true, - trace: '', - - isTraceScrolledToBottom: false, - hasBeenScrolled: false, + // Used to check if we should keep the automatic scroll + isScrolledToBottomBeforeReceivingTrace: true, + trace: '', isTraceComplete: false, - traceSize: 0, // todo_fl: needs to be converted into human readable format in components + traceSize: 0, isTraceSizeVisible: false, - fetchingStatusFavicon: false, - // used as a query parameter + // used as a query parameter to fetch the trace traceState: null, - // used to check if we need to redirect the user - todo_fl: check if actually needed - traceStatus: null, - // sidebar dropdown + // sidebar dropdown & list of jobs isLoadingStages: false, isLoadingJobs: false, selectedStage: __('More'), diff --git a/app/assets/javascripts/jobs/svg/scroll_down.svg b/app/assets/javascripts/jobs/svg/scroll_down.svg new file mode 100644 index 00000000000..1d22870ec09 --- /dev/null +++ b/app/assets/javascripts/jobs/svg/scroll_down.svg @@ -0,0 +1,5 @@ +<svg width="12" height="16" viewBox="0 0 12 16" xmlns="http://www.w3.org/2000/svg"> + <path class="first-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043c.124 0 .23-.035.321-.105.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/> + <path class="second-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/> + <path class="third-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91A.458.458 0 0 1 6.257 6h-.37a.626.626 0 0 1-.136-.09"/> +</svg> diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 2c995c5902f..062501d1d04 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -47,7 +47,10 @@ export default class LabelManager { } toggleEmptyState($label, $btn, action) { - this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); + this.emptyState.classList.toggle( + 'hidden', + !!this.prioritizedLabels[0].querySelector(':scope > li'), + ); } toggleLabelPriority($label, action, persistState) { @@ -80,16 +83,14 @@ export default class LabelManager { return; } if (action === 'remove') { - axios.delete(url) - .catch(rollbackLabelPosition); + axios.delete(url).catch(rollbackLabelPosition); // Restore empty message if (!$from.find('li').length) { $from.find('.empty-message').removeClass('hidden'); } } else { - this.savePrioritySort($label, action) - .catch(rollbackLabelPosition); + this.savePrioritySort($label, action).catch(rollbackLabelPosition); } } @@ -102,8 +103,7 @@ export default class LabelManager { } onPrioritySortUpdate() { - this.savePrioritySort() - .catch(() => flash(this.errorMessage)); + this.savePrioritySort().catch(() => flash(this.errorMessage)); } savePrioritySort() { diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index d85ae851706..2bc09237495 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -22,7 +22,7 @@ export default class Labels { updateColorPreview() { const previewColor = $('input#label_color').val(); return $('div.label-color-preview').css('background-color', previewColor); - // Updates the the preview color with the hex-color input + // Updates the the preview color with the hex-color input } // Updates the preview color with a click on a suggested color diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index e3177188772..b8c3c237eb3 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -5,7 +5,9 @@ import initFlyOutNav from './fly_out_nav'; function hideEndFade($scrollingTabs) { $scrollingTabs.each(function scrollTabsLoop() { const $this = $(this); - $this.siblings('.fade-right').toggleClass('scrolling', Math.round($this.width()) < $this.prop('scrollWidth')); + $this + .siblings('.fade-right') + .toggleClass('scrolling', Math.round($this.width()) < $this.prop('scrollWidth')); }); } @@ -15,38 +17,42 @@ export default function initLayoutNav() { initFlyOutNav(); - $(document).on('init.scrolling-tabs', () => { - const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); - $scrollingTabs.addClass('is-initialized'); + $(document) + .on('init.scrolling-tabs', () => { + const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); + $scrollingTabs.addClass('is-initialized'); - $(window).on('resize.nav', () => { - hideEndFade($scrollingTabs); - }).trigger('resize.nav'); + $(window) + .on('resize.nav', () => { + hideEndFade($scrollingTabs); + }) + .trigger('resize.nav'); - $scrollingTabs.on('scroll', function tabsScrollEvent() { - const $this = $(this); - const currentPosition = $this.scrollLeft(); - const maxPosition = $this.prop('scrollWidth') - $this.outerWidth(); + $scrollingTabs.on('scroll', function tabsScrollEvent() { + const $this = $(this); + const currentPosition = $this.scrollLeft(); + const maxPosition = $this.prop('scrollWidth') - $this.outerWidth(); - $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); - $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); - }); + $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); + $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); + }); - $scrollingTabs.each(function scrollTabsEachLoop() { - const $this = $(this); - const scrollingTabWidth = $this.width(); - const $active = $this.find('.active'); - const activeWidth = $active.width(); + $scrollingTabs.each(function scrollTabsEachLoop() { + const $this = $(this); + const scrollingTabWidth = $this.width(); + const $active = $this.find('.active'); + const activeWidth = $active.width(); - if ($active.length) { - const offset = $active.offset().left + activeWidth; + if ($active.length) { + const offset = $active.offset().left + activeWidth; - if (offset > scrollingTabWidth - 30) { - const scrollLeft = (offset - (scrollingTabWidth / 2)) - (activeWidth / 2); + if (offset > scrollingTabWidth - 30) { + const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2; - $this.scrollLeft(scrollLeft); + $this.scrollLeft(scrollLeft); + } } - } - }); - }).trigger('init.scrolling-tabs'); + }); + }) + .trigger('init.scrolling-tabs'); } diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js index 9cdc0309503..e90b3d2eec7 100644 --- a/app/assets/javascripts/lib/ace.js +++ b/app/assets/javascripts/lib/ace.js @@ -1,3 +1,4 @@ /*= require ace/ace */ +/*= require ace/ext-modelist */ /*= require ace/ext-searchbox */ /*= require ./ace/ace_config_paths */ diff --git a/app/assets/javascripts/lib/utils/ace_utils.js b/app/assets/javascripts/lib/utils/ace_utils.js new file mode 100644 index 00000000000..efc4b2a8d94 --- /dev/null +++ b/app/assets/javascripts/lib/utils/ace_utils.js @@ -0,0 +1,6 @@ +/* global ace */ + +export default function getModeByFileExtension(path) { + const modelist = ace.require("ace/ext/modelist"); + return modelist.getModeForPath(path).mode; +}; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 250980919dd..7cc7cd6d20e 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -53,10 +53,7 @@ export const slugify = str => str.trim().toLowerCase(); * @param {String} str * @returns {String} */ -export const slugifyWithHyphens = str => { - const regex = new RegExp(/\s+/, 'g'); - return str.toLowerCase().replace(regex, '-'); -}; +export const slugifyWithHyphens = str => str.toLowerCase().replace(/\s+/g, '-'); /** * Truncates given text diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index d58fd63bb33..4db63c841a9 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -70,7 +70,7 @@ LineHighlighter.prototype.highlightHash = function(newHash) { const scrollOptions = { // Scroll to the first highlighted line on initial load // Offset -50 for the sticky top bar, and another -100 for some context - offset: -150 + offset: -150, }; if (this.options.scrollFileHolder) { $(this.options.fileHolderSelector).scrollTo(lineSelector, scrollOptions); @@ -85,7 +85,9 @@ LineHighlighter.prototype.clickHandler = function(event) { var current, lineNumber, range; event.preventDefault(); this.clearHighlight(); - lineNumber = $(event.target).closest('a').data('lineNumber'); + lineNumber = $(event.target) + .closest('a') + .data('lineNumber'); current = this.hashToRange(this._hash); if (!(current[0] && event.shiftKey)) { // If there's no current selection, or there is but Shift wasn't held, @@ -104,7 +106,7 @@ LineHighlighter.prototype.clickHandler = function(event) { }; LineHighlighter.prototype.clearHighlight = function() { - return $("." + this.highlightLineClass).removeClass(this.highlightLineClass); + return $('.' + this.highlightLineClass).removeClass(this.highlightLineClass); }; // Convert a URL hash String into line numbers @@ -135,7 +137,7 @@ LineHighlighter.prototype.hashToRange = function(hash) { // // lineNumber - Line number to highlight LineHighlighter.prototype.highlightLine = function(lineNumber) { - return $("#LC" + lineNumber).addClass(this.highlightLineClass); + return $('#LC' + lineNumber).addClass(this.highlightLineClass); }; // Highlight all lines within a range @@ -160,9 +162,9 @@ LineHighlighter.prototype.highlightRange = function(range) { LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { var hash; if (lastLineNumber) { - hash = "#L" + firstLineNumber + "-" + lastLineNumber; + hash = '#L' + firstLineNumber + '-' + lastLineNumber; } else { - hash = "#L" + firstLineNumber; + hash = '#L' + firstLineNumber; } this._hash = hash; return this.__setLocationHash__(hash); @@ -172,11 +174,15 @@ LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { // // This method is stubbed in tests. LineHighlighter.prototype.__setLocationHash__ = function(value) { - return window.history.pushState({ - url: value - // We're using pushState instead of assigning location.hash directly to - // prevent the page from scrolling on the hashchange event - }, document.title, value); + return window.history.pushState( + { + url: value, + // We're using pushState instead of assigning location.hash directly to + // prevent the page from scrolling on the hashchange event + }, + document.title, + value, + ); }; export default LineHighlighter; diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js index 599104dcfa0..5246c49842e 100644 --- a/app/assets/javascripts/locale/sprintf.js +++ b/app/assets/javascripts/locale/sprintf.js @@ -15,7 +15,7 @@ export default (input, parameters, escapeParameters = true) => { let output = input; if (parameters) { - Object.keys(parameters).forEach((parameterName) => { + Object.keys(parameters).forEach(parameterName => { const parameterValue = parameters[parameterName]; const escapedParameterValue = escapeParameters ? _.escape(parameterValue) : parameterValue; output = output.replace(new RegExp(`%{${parameterName}}`, 'g'), escapedParameterValue); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index e8aac51a299..a88b575ad99 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -202,7 +202,6 @@ document.addEventListener('DOMContentLoaded', () => { $('.navbar-toggler').on('click', () => { $('.header-content').toggleClass('menu-expanded'); - gl.lazyLoader.loadCheck(); }); // Show/hide comments on diff diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js index d27922a2099..df5cd1b8c51 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js @@ -9,7 +9,9 @@ import { parsePikadayDate, pikadayToString } from './lib/utils/datefix'; // export default function memberExpirationDate(selector = '.js-access-expiration-date') { function toggleClearInput() { - $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); + $(this) + .closest('.clearable-input') + .toggleClass('has-value', $(this).val() !== ''); } const inputs = $(selector); @@ -40,7 +42,9 @@ export default function memberExpirationDate(selector = '.js-access-expiration-d inputs.next('.js-clear-input').on('click', function clicked(event) { event.preventDefault(); - const input = $(this).closest('.clearable-input').find(selector); + const input = $(this) + .closest('.clearable-input') + .find(selector); const calendar = input.data('pikaday'); calendar.setDate(null); diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index 425b806e9d6..a62ebe23646 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js @@ -5,6 +5,7 @@ import Vue from 'vue'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; import { __ } from '~/locale'; +import getModeByFileExtension from '~/lib/utils/ace_utils'; (global => { global.mergeConflicts = global.mergeConflicts || {}; @@ -72,7 +73,7 @@ import { __ } from '~/locale'; this.fileLoaded = true; this.editor = ace.edit(content); this.editor.$blockScrolling = Infinity; // Turn off annoying warning - this.editor.getSession().setMode(`ace/mode/${data.blob_ace_mode}`); + this.editor.getSession().setMode(getModeByFileExtension(data.new_path)); this.editor.on('change', () => { this.saveDiffResolution(); }); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 9b6d7d1772f..0deae478deb 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -16,26 +16,29 @@ function MergeRequest(opts) { this.opts = opts != null ? opts : {}; this.submitNoteForm = this.submitNoteForm.bind(this); this.$el = $('.merge-request'); - this.$('.show-all-commits').on('click', (function(_this) { - return function() { - return _this.showAllCommits(); - }; - })(this)); + this.$('.show-all-commits').on( + 'click', + (function(_this) { + return function() { + return _this.showAllCommits(); + }; + })(this), + ); this.initTabs(); this.initMRBtnListeners(); this.initCommitMessageListeners(); this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport(); - if ($("a.btn-close").length) { + if ($('a.btn-close').length) { this.taskList = new TaskList({ dataType: 'merge_request', fieldName: 'description', selector: '.detail-page-description', - onSuccess: (result) => { + onSuccess: result => { document.querySelector('#task_status').innerText = result.task_status; document.querySelector('#task_status_short').innerText = result.task_status_short; - } + }, }); } } @@ -84,7 +87,7 @@ MergeRequest.prototype.initMRBtnListeners = function() { MergeRequest.prototype.submitNoteForm = function(form, $button) { var noteText; - noteText = form.find("textarea.js-note-text").val(); + noteText = form.find('textarea.js-note-text').val(); if (noteText.trim().length > 0) { form.submit(); $button.data('submitted', true); @@ -122,7 +125,7 @@ MergeRequest.setStatusBoxToMerged = function() { MergeRequest.decreaseCounter = function(by = 1) { const $el = $('.js-merge-counter'); - const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0); + const count = Math.max(parseInt($el.text().replace(/[^\d]/, ''), 10) - by, 0); $el.text(addDelimiter(count)); }; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 6da04020881..f211632cf24 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -15,7 +15,7 @@ export default class Milestone { } bindTabsSwitching() { - return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { + return $('a[data-toggle="tab"]').on('show.bs.tab', e => { const $target = $(e.target); window.location.hash = $target.attr('href'); @@ -36,7 +36,8 @@ export default class Milestone { const tabElId = $target.attr('href'); if (endpoint && !$target.hasClass('is-loaded')) { - axios.get(endpoint) + axios + .get(endpoint) .then(({ data }) => { $(tabElId).html(data.html); $target.addClass('is-loaded'); @@ -46,23 +47,28 @@ export default class Milestone { } static initDeprecationMessage() { - const deprecationMesssageContainer = document.querySelector('.js-milestone-deprecation-message'); + const deprecationMesssageContainer = document.querySelector( + '.js-milestone-deprecation-message', + ); if (!deprecationMesssageContainer) return; - const deprecationMessage = deprecationMesssageContainer.querySelector('.js-milestone-deprecation-message-template').innerHTML; + const deprecationMessage = deprecationMesssageContainer.querySelector( + '.js-milestone-deprecation-message-template', + ).innerHTML; const $popover = $('.js-popover-link', deprecationMesssageContainer); const hideOnScroll = togglePopover.bind($popover, false); - $popover.popover({ - content: deprecationMessage, - html: true, - placement: 'bottom', - }) - .on('mouseenter', mouseenter) - .on('mouseleave', debouncedMouseleave()) - .on('show.bs.popover', () => { - window.addEventListener('scroll', hideOnScroll, { once: true }); - }); + $popover + .popover({ + content: deprecationMessage, + html: true, + placement: 'bottom', + }) + .on('mouseenter', mouseenter) + .on('mouseleave', debouncedMouseleave()) + .on('show.bs.popover', () => { + window.addEventListener('scroll', hideOnScroll, { once: true }); + }); } } diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js index f8257b6abab..81ab9d8be4b 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js @@ -46,7 +46,7 @@ export default class MiniPipelineGraph { $(document).on( 'click', `${this.container} .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item`, - (e) => { + e => { e.stopPropagation(); }, ); @@ -82,7 +82,8 @@ export default class MiniPipelineGraph { this.renderBuildsList(button, ''); this.toggleLoading(button); - axios.get(endpoint) + axios + .get(endpoint) .then(({ data }) => { this.toggleLoading(button); this.renderBuildsList(button, data.html); @@ -90,7 +91,11 @@ export default class MiniPipelineGraph { }) .catch(() => { this.toggleLoading(button); - if ($(button).parent().hasClass('open')) { + if ( + $(button) + .parent() + .hasClass('open') + ) { $(button).dropdown('toggle'); } flash('An error occurred while fetching the builds.', 'alert'); @@ -104,8 +109,8 @@ export default class MiniPipelineGraph { * @return {type} */ toggleLoading(stageContainer) { - stageContainer.parentElement.querySelector( - `${this.dropdownListSelector} .js-builds-dropdown-loading`, - ).classList.toggle('hidden'); + stageContainer.parentElement + .querySelector(`${this.dropdownListSelector} .js-builds-dropdown-loading`) + .classList.toggle('hidden'); } } diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index ec4c0910e92..cba6759ebf5 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -14,14 +14,14 @@ export default class NamespaceSelect { selectable: true, filterRemote: true, search: { - fields: ['path'] + fields: ['path'], }, fieldName: fieldName, toggleLabel: function(selected) { if (selected.id == null) { return selected.text; } else { - return selected.kind + ": " + selected.full_path; + return selected.kind + ': ' + selected.full_path; } }, data: function(term, dataCallback) { @@ -29,7 +29,7 @@ export default class NamespaceSelect { if (isFilter) { const anyNamespace = { text: 'Any namespace', - id: null + id: null, }; namespaces.unshift(anyNamespace); namespaces.splice(1, 0, 'divider'); @@ -41,7 +41,7 @@ export default class NamespaceSelect { if (namespace.id == null) { return namespace.text; } else { - return namespace.kind + ": " + namespace.full_path; + return namespace.kind + ': ' + namespace.full_path; } }, renderRow: this.renderRow, diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index ad7136adb8c..d1fa9f5e2a2 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -20,7 +20,7 @@ export default (function() { this.mtime = 0; this.mspace = 0; this.parents = {}; - this.colors = ["#000"]; + this.colors = ['#000']; this.offsetX = 150; this.offsetY = 20; this.unitTime = 30; @@ -30,9 +30,10 @@ export default (function() { } BranchGraph.prototype.load = function() { - axios.get(this.options.url) + axios + .get(this.options.url) .then(({ data }) => { - $(".loading", this.element).hide(); + $('.loading', this.element).hide(); this.prepareData(data.days, data.commits); this.buildGraph(); }) @@ -71,17 +72,19 @@ export default (function() { c = ref[j]; this.mtime = Math.max(this.mtime, c.time); this.mspace = Math.max(this.mspace, c.space); - results.push((function() { - var l, len1, ref1, results1; - ref1 = c.parents; - results1 = []; - for (l = 0, len1 = ref1.length; l < len1; l += 1) { - p = ref1[l]; - this.parents[p[0]] = true; - results1.push(this.mspace = Math.max(this.mspace, p[1])); - } - return results1; - }).call(this)); + results.push( + function() { + var l, len1, ref1, results1; + ref1 = c.parents; + results1 = []; + for (l = 0, len1 = ref1.length; l < len1; l += 1) { + p = ref1[l]; + this.parents[p[0]] = true; + results1.push((this.mspace = Math.max(this.mspace, p[1]))); + } + return results1; + }.call(this), + ); } return results; }; @@ -91,11 +94,11 @@ export default (function() { k = 0; results = []; while (k < this.mspace) { - this.colors.push(Raphael.getColor(.8)); + this.colors.push(Raphael.getColor(0.8)); // Skipping a few colors in the spectrum to get more contrast between colors Raphael.getColor(); Raphael.getColor(); - results.push(k += 1); + results.push((k += 1)); } return results; }; @@ -104,12 +107,12 @@ export default (function() { var cuday, cumonth, day, j, len, mm, ref; const { r } = this; cuday = 0; - cumonth = ""; + cumonth = ''; r.rect(0, 0, 40, this.barHeight).attr({ - fill: "#222" + fill: '#222', }); r.rect(40, 0, 30, this.barHeight).attr({ - fill: "#444" + fill: '#444', }); ref = this.days; @@ -118,16 +121,16 @@ export default (function() { if (cuday !== day[0] || cumonth !== day[1]) { // Dates r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({ - font: "12px Monaco, monospace", - fill: "#BBB" + font: '12px Monaco, monospace', + fill: '#BBB', }); [cuday] = day; } if (cumonth !== day[1]) { // Months r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({ - font: "12px Monaco, monospace", - fill: "#EEE" + font: '12px Monaco, monospace', + fill: '#EEE', }); // eslint-disable-next-line prefer-destructuring @@ -173,11 +176,13 @@ export default (function() { BranchGraph.prototype.bindEvents = function() { const { element } = this; - return $(element).scroll((function(_this) { - return function(event) { - return _this.renderPartialGraph(); - }; - })(this)); + return $(element).scroll( + (function(_this) { + return function(event) { + return _this.renderPartialGraph(); + }; + })(this), + ); }; BranchGraph.prototype.scrollDown = function() { @@ -219,46 +224,53 @@ export default (function() { shortrefs = commit.refs; // Truncate if longer than 15 chars if (shortrefs.length > 17) { - shortrefs = shortrefs.substr(0, 15) + "…"; + shortrefs = shortrefs.substr(0, 15) + '…'; } text = r.text(x + 4, y, shortrefs).attr({ - "text-anchor": "start", - font: "10px Monaco, monospace", - fill: "#FFF", - title: commit.refs + 'text-anchor': 'start', + font: '10px Monaco, monospace', + fill: '#FFF', + title: commit.refs, }); textbox = text.getBBox(); // Create rectangle based on the size of the textbox rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({ - fill: "#000", - "fill-opacity": .5, - stroke: "none" + fill: '#000', + 'fill-opacity': 0.5, + stroke: 'none', }); - triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({ - fill: "#000", - "fill-opacity": .5, - stroke: "none" + triangle = r.path(['M', x - 5, y, 'L', x - 15, y - 4, 'L', x - 15, y + 4, 'Z']).attr({ + fill: '#000', + 'fill-opacity': 0.5, + stroke: 'none', }); label = r.set(rect, text); - label.transform(["t", -rect.getBBox().width - 15, 0]); + label.transform(['t', -rect.getBBox().width - 15, 0]); // Set text to front return text.toFront(); }; BranchGraph.prototype.appendAnchor = function(x, y, commit) { const { r, top, options } = this; - const anchor = r.circle(x, y, 10).attr({ - fill: "#000", - opacity: 0, - cursor: "pointer" - }).click(function() { - return window.open(options.commit_url.replace("%s", commit.id), "_blank"); - }).hover(function() { - this.tooltip = r.commitTooltip(x + 5, y, commit); - return top.push(this.tooltip.insertBefore(this)); - }, function() { - return this.tooltip && this.tooltip.remove() && delete this.tooltip; - }); + const anchor = r + .circle(x, y, 10) + .attr({ + fill: '#000', + opacity: 0, + cursor: 'pointer', + }) + .click(function() { + return window.open(options.commit_url.replace('%s', commit.id), '_blank'); + }) + .hover( + function() { + this.tooltip = r.commitTooltip(x + 5, y, commit); + return top.push(this.tooltip.insertBefore(this)); + }, + function() { + return this.tooltip && this.tooltip.remove() && delete this.tooltip; + }, + ); return top.push(anchor); }; @@ -266,7 +278,7 @@ export default (function() { const { r } = this; r.circle(x, y, 3).attr({ fill: this.colors[commit.space], - stroke: "none" + stroke: 'none', }); const avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10; @@ -274,13 +286,15 @@ export default (function() { r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({ stroke: this.colors[commit.space], - "stroke-width": 2 + 'stroke-width': 2, }); r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20); - return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({ - "text-anchor": "start", - font: "14px Monaco, monospace" - }); + return r + .text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split('\n')[0]) + .attr({ + 'text-anchor': 'start', + font: '14px Monaco, monospace', + }); }; BranchGraph.prototype.drawLines = function(x, y, commit) { @@ -304,30 +318,32 @@ export default (function() { // Build line shape if (parent[1] === commit.space) { offset = [0, 5]; - arrow = "l-2,5,4,0,-2,-5,0,5"; + arrow = 'l-2,5,4,0,-2,-5,0,5'; } else if (parent[1] < commit.space) { offset = [3, 3]; - arrow = "l5,0,-2,4,-3,-4,4,2"; + arrow = 'l5,0,-2,4,-3,-4,4,2'; } else { offset = [-3, 3]; - arrow = "l-5,0,2,4,3,-4,-4,2"; + arrow = 'l-5,0,2,4,3,-4,-4,2'; } // Start point - route = ["M", x + offset[0], y + offset[1]]; + route = ['M', x + offset[0], y + offset[1]]; // Add arrow if not first parent if (i > 0) { route.push(arrow); } // Circumvent if overlap if (commit.space !== parentCommit.space || commit.space !== parent[1]) { - route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5); + route.push('L', parentX2, y + 10, 'L', parentX2, parentY - 5); } // End point - route.push("L", parentX1, parentY); - results.push(r.path(route).attr({ - stroke: color, - "stroke-width": 2 - })); + route.push('L', parentX1, parentY); + results.push( + r.path(route).attr({ + stroke: color, + 'stroke-width': 2, + }), + ); } return results; }; @@ -337,10 +353,10 @@ export default (function() { const { r } = this; const x = this.offsetX + this.unitSpace * (this.mspace - commit.space); const y = this.offsetY + this.unitTime * commit.time; - r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({ - fill: "#000", - "fill-opacity": .5, - stroke: "none" + r.path(['M', x + 5, y, 'L', x + 15, y + 4, 'L', x + 15, y - 4, 'Z']).attr({ + fill: '#000', + 'fill-opacity': 0.5, + stroke: 'none', }); // Displayed in the center return this.element.scrollTop(y - this.graphHeight / 2); diff --git a/app/assets/javascripts/network/raphael.js b/app/assets/javascripts/network/raphael.js index 09dcf716148..22e06a35d91 100644 --- a/app/assets/javascripts/network/raphael.js +++ b/app/assets/javascripts/network/raphael.js @@ -49,7 +49,7 @@ Raphael.prototype.textWrap = function testWrap(t, width) { const s = []; for (let j = 0, len = words.length; j < len; j += 1) { const word = words[j]; - if (x + (word.length * letterWidth) > width) { + if (x + word.length * letterWidth > width) { s.push('\n'); x = 0; } diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 8f6ea9e61c1..f338dbbb0a6 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -30,24 +30,24 @@ export default class NewBranchForm { startsWith = { pattern: /^(\/|\.)/g, prefix: "can't start with", - conjunction: "or" + conjunction: 'or', }; endsWith = { pattern: /(\/|\.|\.lock)$/g, prefix: "can't end in", - conjunction: "or" + conjunction: 'or', }; invalid = { pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g, prefix: "can't contain", - conjunction: ", " + conjunction: ', ', }; single = { pattern: /^@+$/g, prefix: "can't be", - conjunction: "or" + conjunction: 'or', }; - return this.restrictions = [startsWith, invalid, endsWith, single]; + return (this.restrictions = [startsWith, invalid, endsWith, single]); } validate() { @@ -73,7 +73,7 @@ export default class NewBranchForm { return "'" + value + "'"; } }); - return restriction.prefix + " " + (formatted.join(restriction.conjunction)); + return restriction.prefix + ' ' + formatted.join(restriction.conjunction); }; validator = (function(_this) { return function(errors, restriction) { @@ -88,7 +88,7 @@ export default class NewBranchForm { })(this); errors = this.restrictions.reduce(validator, []); if (errors.length > 0) { - errorMessage = $("<span/>").text(errors.join(', ')); + errorMessage = $('<span/>').text(errors.join(', ')); return this.branchNameError.append(errorMessage); } } diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js index 17ec20f1cc1..b142f212eb0 100644 --- a/app/assets/javascripts/new_commit_form.js +++ b/app/assets/javascripts/new_commit_form.js @@ -6,9 +6,7 @@ export default class NewCommitForm { this.branchName = form.find('.js-branch-name'); this.originalBranch = form.find('.js-original-branch'); this.createMergeRequest = form.find('.js-create-merge-request'); - this.createMergeRequestContainer = form.find( - '.js-create-merge-request-container', - ); + this.createMergeRequestContainer = form.find('.js-create-merge-request-container'); this.branchName.keyup(this.renderDestination); this.renderDestination(); } diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 33998394a69..38c43e5fe08 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -102,6 +102,18 @@ export default { }, methods: { ...mapActions(['toggleResolveNote']), + shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState) { + // shouldBeResolved() checks the actual resolution state, + // considering batchComments (EEP), if applicable/enabled. + const newResolvedStateAfterUpdate = + this.shouldBeResolved && this.shouldBeResolved(shouldResolve); + + const shouldToggleState = + newResolvedStateAfterUpdate !== undefined && + beforeSubmitDiscussionState !== newResolvedStateAfterUpdate; + + return shouldResolve || shouldToggleState; + }, handleUpdate(shouldResolve) { const beforeSubmitDiscussionState = this.discussionResolved; this.isSubmitting = true; @@ -109,7 +121,7 @@ export default { this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => { this.isSubmitting = false; - if (shouldResolve) { + if (this.shouldToggleResolved(shouldResolve, beforeSubmitDiscussionState)) { this.resolveHandler(beforeSubmitDiscussionState); } }); diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index 8ff8bb446ad..c4c8cf86cb0 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -18,7 +18,9 @@ export default function notificationsDropdown() { $(document).on('ajax:success', '.notification-form', (e, data) => { if (data.saved) { - $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html); + $(e.currentTarget) + .closest('.js-notification-dropdown') + .replaceWith(data.html); } else { Flash('Failed to save new settings', 'alert'); } diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 00e27ca0e70..45f033f2822 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -22,7 +22,8 @@ export default class NotificationsForm { // eslint-disable-next-line class-methods-use-this showCheckboxLoadingSpinner($parent) { - $parent.addClass('is-loading') + $parent + .addClass('is-loading') .find('.custom-notification-event-loading') .removeClass('fa-check') .addClass('fa-spin fa-spinner') @@ -38,9 +39,12 @@ export default class NotificationsForm { .then(({ data }) => { $checkbox.enable(); if (data.saved) { - $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); + $parent + .find('.custom-notification-event-loading') + .toggleClass('fa-spin fa-spinner fa-check is-done'); setTimeout(() => { - $parent.removeClass('is-loading') + $parent + .removeClass('is-loading') .find('.custom-notification-event-loading') .toggleClass('fa-spin fa-spinner fa-check is-done'); }, 2000); diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 86a43b66cc8..3b58c54b3f4 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -24,22 +24,25 @@ export default { getOld() { this.loading.show(); - axios.get(this.url, { - params: { - limit: this.limit, - offset: this.offset, - }, - }).then(({ data }) => { - this.append(data.count, this.prepareData(data.html)); - this.callback(); + axios + .get(this.url, { + params: { + limit: this.limit, + offset: this.offset, + }, + }) + .then(({ data }) => { + this.append(data.count, this.prepareData(data.html)); + this.callback(); - // keep loading until we've filled the viewport height - if (!this.disable && !this.isScrollable()) { - this.getOld(); - } else { - this.loading.hide(); - } - }).catch(() => this.loading.hide()); + // keep loading until we've filled the viewport height + if (!this.disable && !this.isScrollable()) { + this.getOld(); + } else { + this.loading.hide(); + } + }) + .catch(() => this.loading.hide()); }, append(count, html) { diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 002b2279fcc..d0bce857029 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -2,6 +2,7 @@ import groupAvatar from '~/group_avatar'; import TransferDropdown from '~/groups/transfer_dropdown'; import initConfirmDangerModal from '~/confirm_danger_modal'; import initSettingsPanels from '~/settings_panels'; +import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import { GROUP_BADGE } from '~/badges/constants'; @@ -10,5 +11,8 @@ document.addEventListener('DOMContentLoaded', () => { new TransferDropdown(); // eslint-disable-line no-new initConfirmDangerModal(); initSettingsPanels(); + dirtySubmitFactory( + document.querySelectorAll('.js-general-settings-form, .js-general-permissions-form'), + ); mountBadgeSettings(GROUP_BADGE); }); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index b0345b4e50d..5659e13981a 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,5 +1,5 @@ +import initDismissableCallout from '~/dismissable_callout'; import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; -import PersistentUserCallout from '../../persistent_user_callout'; import Project from './project'; import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation'; @@ -12,9 +12,7 @@ document.addEventListener('DOMContentLoaded', () => { ]; if (newClusterViews.indexOf(page) > -1) { - const callout = document.querySelector('.gcp-signup-offer'); - if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new - + initDismissableCallout('.gcp-signup-offer'); initGkeDropdowns(); } diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js index 3626f3ffec6..d57dbeb1242 100644 --- a/app/assets/javascripts/pages/projects/jobs/show/index.js +++ b/app/assets/javascripts/pages/projects/jobs/show/index.js @@ -1,3 +1,3 @@ -import initJobDetails from '~/jobs/job_details_bundle'; +import initJobDetails from '~/jobs'; document.addEventListener('DOMContentLoaded', initJobDetails); diff --git a/app/assets/javascripts/pages/root/index.js b/app/assets/javascripts/pages/root/index.js deleted file mode 100644 index 09f8185d3b5..00000000000 --- a/app/assets/javascripts/pages/root/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// if the "projects dashboard" is a user's default dashboard, when they visit the -// instance root index, the dashboard will be served by the root controller instead -// of a dashboard controller. The root index redirects for all other default dashboards. - -import '../dashboard/projects/index'; diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js index 0009419cd0c..2ed177be558 100644 --- a/app/assets/javascripts/pages/users/user_overview_block.js +++ b/app/assets/javascripts/pages/users/user_overview_block.js @@ -1,10 +1,15 @@ import axios from '~/lib/utils/axios_utils'; +const DEFAULT_LIMIT = 20; + export default class UserOverviewBlock { constructor(options = {}) { this.container = options.container; this.url = options.url; - this.limit = options.limit || 20; + this.requestParams = { + limit: DEFAULT_LIMIT, + ...options.requestParams, + }; this.loadData(); } @@ -15,9 +20,7 @@ export default class UserOverviewBlock { axios .get(this.url, { - params: { - limit: this.limit, - }, + params: this.requestParams, }) .then(({ data }) => this.render(data)) .catch(() => loadingEl.classList.add('hide')); @@ -34,7 +37,9 @@ export default class UserOverviewBlock { if (count && count > 0) { document.querySelector(`${this.container} .js-view-all`).classList.remove('hide'); } else { - document.querySelector(`${this.container} .nothing-here-block`).classList.add('text-left', 'p-0'); + document + .querySelector(`${this.container} .nothing-here-block`) + .classList.add('text-left', 'p-0'); } loadingEl.classList.add('hide'); diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 23b0348a99f..1de9945baad 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -182,18 +182,22 @@ export default class UserTabs { this.loadActivityCalendar('overview'); - UserTabs.renderMostRecentBlocks('#js-overview .activities-block', 5); - UserTabs.renderMostRecentBlocks('#js-overview .projects-block', 10); + UserTabs.renderMostRecentBlocks('#js-overview .activities-block', { + requestParams: { limit: 5 }, + }); + UserTabs.renderMostRecentBlocks('#js-overview .projects-block', { + requestParams: { limit: 10, skip_pagination: true }, + }); this.loaded.overview = true; } - static renderMostRecentBlocks(container, limit) { + static renderMostRecentBlocks(container, options) { // eslint-disable-next-line no-new new UserOverviewBlock({ container, url: $(`${container} .overview-content-list`).data('href'), - limit, + ...options, }); } @@ -216,7 +220,12 @@ export default class UserTabs { let calendarHint = ''; if (action === 'activity') { - calendarHint = sprintf(__('Summary of issues, merge requests, push events, and comments (Timezone: %{utcFormatted})'), { utcFormatted }); + calendarHint = sprintf( + __( + 'Summary of issues, merge requests, push events, and comments (Timezone: %{utcFormatted})', + ), + { utcFormatted }, + ); } else if (action === 'overview') { calendarHint = __('Issues, merge requests, pushes and comments.'); } @@ -224,7 +233,15 @@ export default class UserTabs { $calendarWrap.find('.calendar-hint').text(calendarHint); // eslint-disable-next-line no-new - new ActivityCalendar('.tab-pane.active .js-contrib-calendar', '.tab-pane.active .user-calendar-activities', data, calendarActivitiesPath, utcOffset, 0, monthsAgo); + new ActivityCalendar( + '.tab-pane.active .js-contrib-calendar', + '.tab-pane.active .user-calendar-activities', + data, + calendarActivitiesPath, + utcOffset, + 0, + monthsAgo, + ); }) .catch(() => flash(__('There was an error loading users activity calendar.'))); } diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index 2f480ecdc69..7b079fe02d9 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -1,57 +1,58 @@ <script> - import pdfjsLib from 'vendor/pdf'; - import workerSrc from 'vendor/pdf.worker.min'; +import pdfjsLib from 'vendor/pdf'; +import workerSrc from 'vendor/pdf.worker.min'; - import page from './page/index.vue'; +import page from './page/index.vue'; - export default { - components: { page }, - props: { - pdf: { - type: [String, Uint8Array], - required: true, - }, +export default { + components: { page }, + props: { + pdf: { + type: [String, Uint8Array], + required: true, }, - data() { - return { - loading: false, - pages: [], - }; + }, + data() { + return { + loading: false, + pages: [], + }; + }, + computed: { + document() { + return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf }; }, - computed: { - document() { - return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf }; - }, - hasPDF() { - return this.pdf && this.pdf.length > 0; - }, + hasPDF() { + return this.pdf && this.pdf.length > 0; }, - watch: { pdf: 'load' }, - mounted() { - pdfjsLib.PDFJS.workerSrc = workerSrc; - if (this.hasPDF) this.load(); + }, + watch: { pdf: 'load' }, + mounted() { + pdfjsLib.PDFJS.workerSrc = workerSrc; + if (this.hasPDF) this.load(); + }, + methods: { + load() { + this.pages = []; + return pdfjsLib + .getDocument(this.document) + .then(this.renderPages) + .then(() => this.$emit('pdflabload')) + .catch(error => this.$emit('pdflaberror', error)) + .then(() => { + this.loading = false; + }); }, - methods: { - load() { - this.pages = []; - return pdfjsLib.getDocument(this.document) - .then(this.renderPages) - .then(() => this.$emit('pdflabload')) - .catch(error => this.$emit('pdflaberror', error)) - .then(() => { this.loading = false; }); - }, - renderPages(pdf) { - const pagePromises = []; - this.loading = true; - for (let num = 1; num <= pdf.numPages; num += 1) { - pagePromises.push( - pdf.getPage(num).then(p => this.pages.push(p)), - ); - } - return Promise.all(pagePromises); - }, + renderPages(pdf) { + const pagePromises = []; + this.loading = true; + for (let num = 1; num <= pdf.numPages; num += 1) { + pagePromises.push(pdf.getPage(num).then(p => this.pages.push(p))); + } + return Promise.all(pagePromises); }, - }; + }, +}; </script> <template> @@ -69,9 +70,9 @@ </template> <style> - .pdf-viewer { - background: url('./assets/img/bg.gif'); - display: flex; - flex-flow: column nowrap; - } +.pdf-viewer { + background: url('./assets/img/bg.gif'); + display: flex; + flex-flow: column nowrap; +} </style> diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue index 9f06833d560..96aadf41653 100644 --- a/app/assets/javascripts/pdf/page/index.vue +++ b/app/assets/javascripts/pdf/page/index.vue @@ -1,44 +1,47 @@ <script> - export default { - props: { - page: { - type: Object, - required: true, - }, - number: { - type: Number, - required: true, - }, +export default { + props: { + page: { + type: Object, + required: true, }, - data() { - return { - scale: 4, - rendering: false, - }; + number: { + type: Number, + required: true, + }, + }, + data() { + return { + scale: 4, + rendering: false, + }; + }, + computed: { + viewport() { + return this.page.getViewport(this.scale); }, - computed: { - viewport() { - return this.page.getViewport(this.scale); - }, - context() { - return this.$refs.canvas.getContext('2d'); - }, - renderContext() { - return { - canvasContext: this.context, - viewport: this.viewport, - }; - }, + context() { + return this.$refs.canvas.getContext('2d'); }, - mounted() { - this.$refs.canvas.height = this.viewport.height; - this.$refs.canvas.width = this.viewport.width; - this.rendering = true; - this.page.render(this.renderContext) - .then(() => { this.rendering = false; }) - .catch(error => this.$emit('pdflaberror', error)); + renderContext() { + return { + canvasContext: this.context, + viewport: this.viewport, + }; }, - }; + }, + mounted() { + this.$refs.canvas.height = this.viewport.height; + this.$refs.canvas.width = this.viewport.width; + this.rendering = true; + this.page + .render(this.renderContext) + .then(() => { + this.rendering = false; + }) + .catch(error => this.$emit('pdflaberror', error)); + }, +}; </script> <template> @@ -51,20 +54,20 @@ </template> <style> - .pdf-page { - margin: 8px auto 0 auto; - border-top: 1px #ddd solid; - border-bottom: 1px #ddd solid; - width: 100%; - } +.pdf-page { + margin: 8px auto 0 auto; + border-top: 1px #ddd solid; + border-bottom: 1px #ddd solid; + width: 100%; +} - .pdf-page:first-child { - margin-top: 0px; - border-top: 0px; - } +.pdf-page:first-child { + margin-top: 0px; + border-top: 0px; +} - .pdf-page:last-child { - margin-bottom: 0px; - border-bottom: 0px; - } +.pdf-page:last-child { + margin-bottom: 0px; + border-bottom: 0px; +} </style> diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index 1522e2227e4..300d453c174 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; +import { glEmojiTag } from '~/emoji'; import detailedMetric from './detailed_metric.vue'; import requestSelector from './request_selector.vue'; @@ -64,6 +65,16 @@ export default { lineProfileModal() { return $('#modal-peek-line-profile'); }, + hasHost() { + return this.currentRequest && this.currentRequest.details && this.currentRequest.details.host; + }, + birdEmoji() { + if (this.hasHost && this.currentRequest.details.host.canary) { + return glEmojiTag('baby_chick'); + } + + return ''; + }, }, mounted() { this.currentRequest = this.requestId; @@ -93,9 +104,11 @@ export default { class="view" > <span - v-if="currentRequest.details" + v-if="hasHost" class="current-host" + :class="{ 'canary' : currentRequest.details.host.canary }" > + <span v-html="birdEmoji"></span> {{ currentRequest.details.host.hostname }} </span> </div> diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js deleted file mode 100644 index 1e34e74a152..00000000000 --- a/app/assets/javascripts/persistent_user_callout.js +++ /dev/null @@ -1,34 +0,0 @@ -import axios from './lib/utils/axios_utils'; -import { __ } from './locale'; -import Flash from './flash'; - -export default class PersistentUserCallout { - constructor(container) { - const { dismissEndpoint, featureId } = container.dataset; - this.container = container; - this.dismissEndpoint = dismissEndpoint; - this.featureId = featureId; - - this.init(); - } - - init() { - const closeButton = this.container.querySelector('.js-close'); - closeButton.addEventListener('click', event => this.dismiss(event)); - } - - dismiss(event) { - event.preventDefault(); - - axios - .post(this.dismissEndpoint, { - feature_name: this.featureId, - }) - .then(() => { - this.container.remove(); - }) - .catch(() => { - Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.')); - }); - } -} diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 9b4ba0c1a9a..23c0be7742e 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -70,7 +70,7 @@ export default { v-for="(stage, index) in graph" :key="stage.name" :title="capitalizeStageName(stage.name)" - :jobs="stage.groups" + :groups="stage.groups" :stage-connector-class="stageConnectorClass(index, stage)" :is-first-column="isFirstColumn(index)" @refreshPipelineGraph="refreshPipelineGraph" diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue index 2ad66f4fe86..34bada533df 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue @@ -1,31 +1,14 @@ <script> import $ from 'jquery'; -import JobNameComponent from './job_name_component.vue'; -import JobComponent from './job_component.vue'; +import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import JobItem from './job_item.vue'; import tooltip from '../../../vue_shared/directives/tooltip'; /** * Renders the dropdown for the pipeline graph. * - * The following object should be provided as `job`: + * The object provided as `group` corresponds to app/serializers/job_group_entity.rb. * - * { - * "id": 4256, - * "name": "test", - * "status": { - * "icon": "status_success", - * "text": "passed", - * "label": "passed", - * "group": "success", - * "details_path": "/root/ci-mock/builds/4256", - * "action": { - * "icon": "retry", - * "title": "Retry", - * "path": "/root/ci-mock/builds/4256/retry", - * "method": "post" - * } - * } - * } */ export default { directives: { @@ -33,12 +16,12 @@ export default { }, components: { - JobComponent, - JobNameComponent, + JobItem, + CiIcon, }, props: { - job: { + group: { type: Object, required: true, }, @@ -46,7 +29,8 @@ export default { computed: { tooltipText() { - return `${this.job.name} - ${this.job.status.label}`; + const { name, status } = this.group; + return `${name} - ${status.label}`; }, }, @@ -56,7 +40,7 @@ export default { methods: { /** - * When the user right clicks or cmd/ctrl + click in the job name or the action icon + * When the user right clicks or cmd/ctrl + click in the group name or the action icon * the dropdown should not be closed so we stop propagation * of the click event inside the dropdown. * @@ -90,14 +74,14 @@ export default { data-display="static" class="dropdown-menu-toggle build-content" > + <ci-icon :status="group.status" /> - <job-name-component - :name="job.name" - :status="job.status" - /> + <span class="ci-status-text"> + {{ group.name }} + </span> <span class="dropdown-counter-badge"> - {{ job.size }} + {{ group.size }} </span> </button> @@ -105,12 +89,12 @@ export default { <li class="scrollable-menu"> <ul> <li - v-for="(item, i) in job.jobs" - :key="i" + v-for="job in group.jobs" + :key="job.id" > - <job-component - :dropdown-length="job.size" - :job="item" + <job-item + :dropdown-length="group.size" + :job="job" css-class-job-name="mini-pipeline-graph-dropdown-item" @pipelineActionRequestComplete="pipelineActionRequestComplete" /> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue index a1504592bbc..a1504592bbc 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index 567ea119343..efbab51d200 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -1,12 +1,12 @@ <script> import _ from 'underscore'; -import JobComponent from './job_component.vue'; -import DropdownJobComponent from './dropdown_job_component.vue'; +import JobItem from './job_item.vue'; +import JobGroupDropdown from './job_group_dropdown.vue'; export default { components: { - JobComponent, - DropdownJobComponent, + JobItem, + JobGroupDropdown, }, props: { title: { @@ -14,7 +14,7 @@ export default { required: true, }, - jobs: { + groups: { type: Array, required: true, }, @@ -33,12 +33,8 @@ export default { }, methods: { - firstJob(list) { - return list[0]; - }, - - jobId(job) { - return `ci-badge-${_.escape(job.name)}`; + groupId(group) { + return `ci-badge-${_.escape(group.name)}`; }, buildConnnectorClass(index) { @@ -61,25 +57,25 @@ export default { <div class="builds-container"> <ul> <li - v-for="(job, index) in jobs" - :id="jobId(job)" - :key="job.id" + v-for="(group, index) in groups" + :id="groupId(group)" + :key="group.id" :class="buildConnnectorClass(index)" class="build" > <div class="curve"></div> - <job-component - v-if="job.size === 1" - :job="job" + <job-item + v-if="group.size === 1" + :job="group.jobs[0]" css-class-job-name="build-content" @pipelineActionRequestComplete="pipelineActionRequestComplete" /> - <dropdown-job-component - v-if="job.size > 1" - :job="job" + <job-group-dropdown + v-if="group.size > 1" + :group="group" @pipelineActionRequestComplete="pipelineActionRequestComplete" /> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index b03438ddba1..4f89ee66023 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -64,10 +64,7 @@ export default { return []; } const { details } = this.pipeline; - return [ - ...(details.manual_actions || []), - ...(details.scheduled_actions || []), - ]; + return [...(details.manual_actions || []), ...(details.scheduled_actions || [])]; }, /** * If provided, returns the commit tag. diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 47c15b1a9c4..7ec55792850 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -18,14 +18,14 @@ import Flash from '../../flash'; import axios from '../../lib/utils/axios_utils'; import eventHub from '../event_hub'; import Icon from '../../vue_shared/components/icon.vue'; -import JobComponent from './graph/job_component.vue'; +import JobItem from './graph/job_item.vue'; import tooltip from '../../vue_shared/directives/tooltip'; import { PIPELINES_TABLE } from '../constants'; export default { components: { Icon, - JobComponent, + JobItem, }, directives: { @@ -198,7 +198,7 @@ export default { v-for="job in dropdownContent" :key="job.id" > - <job-component + <job-item :dropdown-length="dropdownContent.length" :job="job" css-class-job-name="mini-pipeline-graph-dropdown-item" diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 12cfa7de316..60d3d83a4b2 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -10,14 +10,14 @@ import { __ } from '~/locale'; const highlighter = function(element, text, matches) { var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched; lastIndex = 0; - highlightText = ""; + highlightText = ''; matchedChars = []; for (j = 0, len = matches.length; j < len; j += 1) { matchIndex = matches[j]; unmatched = text.substring(lastIndex, matchIndex); if (unmatched) { if (matchedChars.length) { - element.append(matchedChars.join("").bold()); + element.append(matchedChars.join('').bold()); } matchedChars = []; element.append(document.createTextNode(unmatched)); @@ -26,7 +26,7 @@ const highlighter = function(element, text, matches) { lastIndex = matchIndex + 1; } if (matchedChars.length) { - element.append(matchedChars.join("").bold()); + element.append(matchedChars.join('').bold()); } return element.append(document.createTextNode(text.substring(lastIndex))); }; @@ -40,7 +40,7 @@ export default class ProjectFindFile { this.selectRowDown = this.selectRowDown.bind(this); this.selectRowUp = this.selectRowUp.bind(this); this.filePaths = {}; - this.inputElement = this.element.find(".file-finder-input"); + this.inputElement = this.element.find('.file-finder-input'); // init event this.initEvent(); // focus text input box @@ -50,38 +50,51 @@ export default class ProjectFindFile { } initEvent() { - this.inputElement.off("keyup"); - this.inputElement.on("keyup", (function(_this) { - return function(event) { - var oldValue, ref, target, value; - target = $(event.target); - value = target.val(); - oldValue = (ref = target.data("oldValue")) != null ? ref : ""; - if (value !== oldValue) { - target.data("oldValue", value); - _this.findFile(); - return _this.element.find("tr.tree-item").eq(0).addClass("selected").focus(); - } - }; - })(this)); + this.inputElement.off('keyup'); + this.inputElement.on( + 'keyup', + (function(_this) { + return function(event) { + var oldValue, ref, target, value; + target = $(event.target); + value = target.val(); + oldValue = (ref = target.data('oldValue')) != null ? ref : ''; + if (value !== oldValue) { + target.data('oldValue', value); + _this.findFile(); + return _this.element + .find('tr.tree-item') + .eq(0) + .addClass('selected') + .focus(); + } + }; + })(this), + ); } findFile() { var result, searchText; searchText = this.inputElement.val(); - result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; + result = + searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; return this.renderList(result, searchText); - // find file + // find file } // files pathes load load(url) { - axios.get(url) + axios + .get(url) .then(({ data }) => { this.element.find('.loading').hide(); this.filePaths = data; this.findFile(); - this.element.find('.files-slider tr.tree-item').eq(0).addClass('selected').focus(); + this.element + .find('.files-slider tr.tree-item') + .eq(0) + .addClass('selected') + .focus(); }) .catch(() => flash(__('An error occurred while loading filenames'))); } @@ -89,7 +102,7 @@ export default class ProjectFindFile { // render result renderList(filePaths, searchText) { var blobItemUrl, filePath, html, i, len, matches, results; - this.element.find(".tree-table > tbody").empty(); + this.element.find('.tree-table > tbody').empty(); results = []; for (i = 0, len = filePaths.length; i < len; i += 1) { @@ -100,9 +113,9 @@ export default class ProjectFindFile { if (searchText) { matches = fuzzaldrinPlus.match(filePath, searchText); } - blobItemUrl = this.options.blobUrlTemplate + "/" + filePath; + blobItemUrl = this.options.blobUrlTemplate + '/' + filePath; html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl); - results.push(this.element.find(".tree-table > tbody").append(html)); + results.push(this.element.find('.tree-table > tbody').append(html)); } return results; } @@ -110,52 +123,56 @@ export default class ProjectFindFile { // make tbody row html static makeHtml(filePath, matches, blobItemUrl) { var $tr; - $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>"); + $tr = $( + "<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>", + ); if (matches) { - $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl)); + $tr + .find('a') + .replaceWith(highlighter($tr.find('a'), filePath, matches).attr('href', blobItemUrl)); } else { - $tr.find("a").attr("href", blobItemUrl); - $tr.find(".str-truncated").text(filePath); + $tr.find('a').attr('href', blobItemUrl); + $tr.find('.str-truncated').text(filePath); } return $tr; } selectRow(type) { var next, rows, selectedRow; - rows = this.element.find(".files-slider tr.tree-item"); - selectedRow = this.element.find(".files-slider tr.tree-item.selected"); + rows = this.element.find('.files-slider tr.tree-item'); + selectedRow = this.element.find('.files-slider tr.tree-item.selected'); if (rows && rows.length > 0) { if (selectedRow && selectedRow.length > 0) { - if (type === "UP") { + if (type === 'UP') { next = selectedRow.prev(); - } else if (type === "DOWN") { + } else if (type === 'DOWN') { next = selectedRow.next(); } if (next.length > 0) { - selectedRow.removeClass("selected"); + selectedRow.removeClass('selected'); selectedRow = next; } } else { selectedRow = rows.eq(0); } - return selectedRow.addClass("selected").focus(); + return selectedRow.addClass('selected').focus(); } } selectRowUp() { - return this.selectRow("UP"); + return this.selectRow('UP'); } selectRowDown() { - return this.selectRow("DOWN"); + return this.selectRow('DOWN'); } goToTree() { - return window.location.href = this.options.treeUrl; + return (window.location.href = this.options.treeUrl); } goToBlob() { - var $link = this.element.find(".tree-item.selected .tree-item-file-name a"); + var $link = this.element.find('.tree-item.selected .tree-item-file-name a'); if ($link.length) { $link.get(0).click(); diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index 5a0d2b642eb..a51a2a2242f 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -5,4 +5,3 @@ export default function projectImport() { visitUrl(window.location.href); }, 5000); } - diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index 9049f87e037..d3c604dcee1 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -31,32 +31,35 @@ export default class ProjectLabelSubscription { $btn.addClass('disabled'); - axios.post(url).then(() => { - let newStatus; - let newAction; + axios + .post(url) + .then(() => { + let newStatus; + let newAction; - if (oldStatus === 'unsubscribed') { - [newStatus, newAction] = ['subscribed', 'Unsubscribe']; - } else { - [newStatus, newAction] = ['unsubscribed', 'Subscribe']; - } + if (oldStatus === 'unsubscribed') { + [newStatus, newAction] = ['subscribed', 'Unsubscribe']; + } else { + [newStatus, newAction] = ['unsubscribed', 'Subscribe']; + } - $btn.removeClass('disabled'); + $btn.removeClass('disabled'); - this.$buttons.attr('data-status', newStatus); - this.$buttons.find('> span').text(newAction); + this.$buttons.attr('data-status', newStatus); + this.$buttons.find('> span').text(newAction); - this.$buttons.map((i, button) => { - const $button = $(button); - const originalTitle = $button.attr('data-original-title'); + this.$buttons.map((i, button) => { + const $button = $(button); + const originalTitle = $button.attr('data-original-title'); - if (originalTitle) { - ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction); - } + if (originalTitle) { + ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction); + } - return button; - }); - }).catch(() => flash(__('There was an error subscribing to this label.'))); + return button; + }); + }) + .catch(() => flash(__('There was an error subscribing to this label.'))); } static setNewTitle($button, originalTitle, newStatus) { diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index eaaeda8b339..f1fff173619 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -16,28 +16,28 @@ export default function projectSelect() { this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled'); this.allowClear = $(select).data('allowClear') || false; - placeholder = "Search for project"; + placeholder = 'Search for project'; if (this.includeGroups) { - placeholder += " or group"; + placeholder += ' or group'; } $(select).select2({ placeholder: placeholder, minimumInputLength: 0, - query: (function (_this) { - return function (query) { + query: (function(_this) { + return function(query) { var finalCallback, projectsCallback; - finalCallback = function (projects) { + finalCallback = function(projects) { var data; data = { - results: projects + results: projects, }; return query.callback(data); }; if (_this.includeGroups) { - projectsCallback = function (projects) { + projectsCallback = function(projects) { var groupsCallback; - groupsCallback = function (groups) { + groupsCallback = function(groups) { var data; data = groups.concat(projects); return finalCallback(data); @@ -48,17 +48,26 @@ export default function projectSelect() { projectsCallback = finalCallback; } if (_this.groupId) { - return Api.groupProjects(_this.groupId, query.term, { - with_issues_enabled: _this.withIssuesEnabled, - with_merge_requests_enabled: _this.withMergeRequestsEnabled, - }, projectsCallback); + return Api.groupProjects( + _this.groupId, + query.term, + { + with_issues_enabled: _this.withIssuesEnabled, + with_merge_requests_enabled: _this.withMergeRequestsEnabled, + }, + projectsCallback, + ); } else { - return Api.projects(query.term, { - order_by: _this.orderBy, - with_issues_enabled: _this.withIssuesEnabled, - with_merge_requests_enabled: _this.withMergeRequestsEnabled, - membership: !_this.allProjects, - }, projectsCallback); + return Api.projects( + query.term, + { + order_by: _this.orderBy, + with_issues_enabled: _this.withIssuesEnabled, + with_merge_requests_enabled: _this.withMergeRequestsEnabled, + membership: !_this.allProjects, + }, + projectsCallback, + ); } }; })(this), @@ -69,7 +78,7 @@ export default function projectSelect() { url: project.web_url, }); }, - text: function (project) { + text: function(project) { return project.name_with_namespace || project.name; }, @@ -79,7 +88,7 @@ export default function projectSelect() { allowClear: this.allowClear, - dropdownCssClass: "ajax-project-dropdown" + dropdownCssClass: 'ajax-project-dropdown', }); if (simpleFilter) return select; return new ProjectSelectComboButton(select); diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js index 9b404896e86..3dbac3ff942 100644 --- a/app/assets/javascripts/project_select_combo_button.js +++ b/app/assets/javascripts/project_select_combo_button.js @@ -14,10 +14,11 @@ export default class ProjectSelectComboButton { } bindEvents() { - this.projectSelectInput.siblings('.new-project-item-select-button') + this.projectSelectInput + .siblings('.new-project-item-select-button') .on('click', e => this.openDropdown(e)); - this.newItemBtn.on('click', (e) => { + this.newItemBtn.on('click', e => { if (!this.getProjectFromLocalStorage()) { e.preventDefault(); this.openDropdown(e); @@ -31,14 +32,21 @@ export default class ProjectSelectComboButton { const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe(); if (localStorageIsSafe) { - this.localStorageKey = ['group', this.groupId, this.formattedText.localStorageItemType, 'recent-project'].join('-'); + this.localStorageKey = [ + 'group', + this.groupId, + this.formattedText.localStorageItemType, + 'recent-project', + ].join('-'); this.setBtnTextFromLocalStorage(); } } // eslint-disable-next-line class-methods-use-this openDropdown(event) { - $(event.currentTarget).siblings('.project-item-select').select2('open'); + $(event.currentTarget) + .siblings('.project-item-select') + .select2('open'); } selectProject() { @@ -86,8 +94,14 @@ export default class ProjectSelectComboButton { const defaultTextPrefix = this.resourceLabel; // the trailing slice call depluralizes each of these strings (e.g. new-issues -> new-issue) - const localStorageItemType = `new-${this.resourceType.split('_').join('-').slice(0, -1)}`; - const presetTextSuffix = this.resourceType.split('_').join(' ').slice(0, -1); + const localStorageItemType = `new-${this.resourceType + .split('_') + .join('-') + .slice(0, -1)}`; + const presetTextSuffix = this.resourceType + .split('_') + .join(' ') + .slice(0, -1); return { localStorageItemType, // new-issue / new-merge-request @@ -96,4 +110,3 @@ export default class ProjectSelectComboButton { }; } } - diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/project_visibility.js index a52ac768e57..aaf6723c85c 100644 --- a/app/assets/javascripts/project_visibility.js +++ b/app/assets/javascripts/project_visibility.js @@ -7,7 +7,7 @@ function setVisibilityOptions(namespaceSelector) { const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex]; const { name, visibility, visibilityLevel, showPath, editPath } = selectedNamespace.dataset; - document.querySelectorAll('.visibility-level-setting .form-check').forEach((option) => { + document.querySelectorAll('.visibility-level-setting .form-check').forEach(option => { const optionInput = option.querySelector('input[type=radio]'); const optionValue = optionInput ? optionInput.value : 0; const optionTitle = option.querySelector('.option-title'); @@ -20,8 +20,7 @@ function setVisibilityOptions(namespaceSelector) { optionInput.disabled = true; const reason = option.querySelector('.option-disabled-reason'); if (reason) { - reason.innerHTML = - `This project cannot be ${optionName} because the visibility of + reason.innerHTML = `This project cannot be ${optionName} because the visibility of <a href="${showPath}">${name}</a> is ${visibility}. To make this project ${optionName}, you must first <a href="${editPath}">change the visibility</a> of the parent group.`; diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index 078ccbbbac2..8380cfb6c59 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -65,10 +65,14 @@ export default class PrometheusMetrics { let totalMissingEnvVarMetrics = 0; let totalExporters = 0; - metrics.forEach((metric) => { + metrics.forEach(metric => { if (metric.active_metrics > 0) { totalExporters += 1; - this.$monitoredMetricsList.append(`<li>${_.escape(metric.group)}<span class="badge">${_.escape(metric.active_metrics)}</span></li>`); + this.$monitoredMetricsList.append( + `<li>${_.escape(metric.group)}<span class="badge">${_.escape( + metric.active_metrics, + )}</span></li>`, + ); totalMonitoredMetrics += metric.active_metrics; if (metric.metrics_missing_requirements > 0) { this.$missingEnvVarMetricsList.append(`<li>${_.escape(metric.group)}</li>`); @@ -78,17 +82,26 @@ export default class PrometheusMetrics { }); if (totalMonitoredMetrics === 0) { - const emptyCommonMetricsText = sprintf(s__('PrometheusService|<p class="text-tertiary">No <a href="%{docsUrl}">common metrics</a> were found</p>'), { - docsUrl: this.helpMetricsPath, - }, false); + const emptyCommonMetricsText = sprintf( + s__( + 'PrometheusService|<p class="text-tertiary">No <a href="%{docsUrl}">common metrics</a> were found</p>', + ), + { + docsUrl: this.helpMetricsPath, + }, + false, + ); this.$monitoredMetricsEmpty.empty(); this.$monitoredMetricsEmpty.append(emptyCommonMetricsText); this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); } else { - const metricsCountText = sprintf(s__('PrometheusService|%{exporters} with %{metrics} were found'), { - exporters: n__('%d exporter', '%d exporters', totalExporters), - metrics: n__('%d metric', '%d metrics', totalMonitoredMetrics), - }); + const metricsCountText = sprintf( + s__('PrometheusService|%{exporters} with %{metrics} were found'), + { + exporters: n__('%d exporter', '%d exporters', totalExporters), + metrics: n__('%d metric', '%d metrics', totalMonitoredMetrics), + }, + ); this.$monitoredMetricsCount.text(metricsCountText); this.showMonitoringMetricsPanelState(PANEL_STATE.LIST); @@ -102,7 +115,8 @@ export default class PrometheusMetrics { loadActiveMetrics() { this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); backOff((next, stop) => { - axios.get(this.activeMetricsEndpoint) + axios + .get(this.activeMetricsEndpoint) .then(({ data }) => { if (data && data.success) { stop(data); @@ -117,7 +131,7 @@ export default class PrometheusMetrics { }) .catch(stop); }) - .then((res) => { + .then(res => { if (res && res.data && res.data.length) { this.populateActiveMetrics(res.data); } else { diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js index 658caeecde1..338006ce2b9 100644 --- a/app/assets/javascripts/raven/raven_config.js +++ b/app/assets/javascripts/raven/raven_config.js @@ -9,7 +9,7 @@ const IGNORE_ERRORS = [ 'canvas.contentDocument', 'MyApp_RemoveAllHighlights', 'http://tt.epicplay.com', - 'Can\'t find variable: ZiteReader', + "Can't find variable: ZiteReader", 'jigsaw is not defined', 'ComboSearch is not defined', 'http://loading.retry.widdit.com/', diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js index 95c5cf7b345..75bac035aca 100644 --- a/app/assets/javascripts/ref_select_dropdown.js +++ b/app/assets/javascripts/ref_select_dropdown.js @@ -2,7 +2,8 @@ import $ from 'jquery'; class RefSelectDropdown { constructor($dropdownButton, availableRefs) { - const availableRefsValue = availableRefs || JSON.parse(document.getElementById('availableRefs').innerHTML); + const availableRefsValue = + availableRefs || JSON.parse(document.getElementById('availableRefs').innerHTML); $dropdownButton.glDropdown({ data: availableRefsValue, filterable: true, @@ -29,7 +30,7 @@ class RefSelectDropdown { const $fieldInput = $(`input[name="${$dropdownButton.data('fieldName')}"]`, $dropdownContainer); const $filterInput = $('input[type="search"]', $dropdownContainer); - $filterInput.on('keyup', (e) => { + $filterInput.on('keyup', e => { const keyCode = e.keyCode || e.which; if (keyCode !== 13) return; diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index fb8c6402d02..b373d83a44b 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -82,7 +82,7 @@ :loading-text="groupedSummaryText" :error-text="groupedSummaryText" :has-issues="reports.length > 0" - class="mr-widget-border-top grouped-security-reports mr-report" + class="mr-widget-section grouped-security-reports mr-report" > <div slot="body" diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 64f3dde5be7..6b3753f7966 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -57,8 +57,6 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); - - if (gl.lazyLoader) gl.lazyLoader.loadCheck(); } $this.attr('data-original-title', tooltipLabel); diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index 37b4a2a4c63..d22aca35e09 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -1,21 +1,27 @@ import $ from 'jquery'; +import { __ } from './locale'; function expandSection($section) { - $section.find('.js-settings-toggle').text('Collapse'); - $section.find('.settings-content').off('scroll.expandSection').scrollTop(0); + $section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Collapse')); + $section + .find('.settings-content') + .off('scroll.expandSection') + .scrollTop(0); $section.addClass('expanded'); if (!$section.hasClass('no-animate')) { - $section.addClass('animating') + $section + .addClass('animating') .one('animationend.animateSection', () => $section.removeClass('animating')); } } function closeSection($section) { - $section.find('.js-settings-toggle').text('Expand'); + $section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Expand')); $section.find('.settings-content').on('scroll.expandSection', () => expandSection($section)); $section.removeClass('expanded'); if (!$section.hasClass('no-animate')) { - $section.addClass('animating') + $section + .addClass('animating') .one('animationend.animateSection', () => $section.removeClass('animating')); } } diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index ab7fab7e5ca..cdff4105335 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -102,7 +102,6 @@ export default { > <icon :name="lockIcon" - aria-hidden="true" class="sidebar-item-icon is-active" /> </div> @@ -134,7 +133,6 @@ export default { <icon :size="16" name="lock" - aria-hidden="true" class="sidebar-item-icon inline is-active" /> {{ __('Locked') }} @@ -147,7 +145,6 @@ export default { <icon :size="16" name="lock-open" - aria-hidden="true" class="sidebar-item-icon inline" /> {{ __('Unlocked') }} diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 99c93952e2a..f2b9d75dd00 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -10,8 +10,10 @@ import syntaxHighlight from './syntax_highlight'; const WRAPPER = '<div class="diff-content"></div>'; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; -const ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; -const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <button class="click-to-expand btn btn-link">Click to expand it.</button></div>'; +const ERROR_HTML = + '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; +const COLLAPSED_HTML = + '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <button class="click-to-expand btn btn-link">Click to expand it.</button></div>'; export default class SingleFileDiff { constructor(file) { @@ -23,23 +25,36 @@ export default class SingleFileDiff { this.isOpen = !this.diffForPath; if (this.diffForPath) { this.collapsedContent = this.content; - this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide(); + this.loadingContent = $(WRAPPER) + .addClass('loading') + .html(LOADING_HTML) + .hide(); this.content = null; this.collapsedContent.after(this.loadingContent); this.$toggleIcon.addClass('fa-caret-right'); } else { - this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide(); + this.collapsedContent = $(WRAPPER) + .html(COLLAPSED_HTML) + .hide(); this.content.after(this.collapsedContent); this.$toggleIcon.addClass('fa-caret-down'); } - $('.js-file-title, .click-to-expand', this.file).on('click', (function (e) { - this.toggleDiff($(e.target)); - }).bind(this)); + $('.js-file-title, .click-to-expand', this.file).on( + 'click', + function(e) { + this.toggleDiff($(e.target)); + }.bind(this), + ); } toggleDiff($target, cb) { - if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; + if ( + !$target.hasClass('js-file-title') && + !$target.hasClass('click-to-expand') && + !$target.hasClass('diff-toggle-caret') + ) + return; this.isOpen = !this.isOpen; if (!this.isOpen && !this.hasError) { this.content.hide(); @@ -65,7 +80,8 @@ export default class SingleFileDiff { this.collapsedContent.hide(); this.loadingContent.show(); - axios.get(this.diffForPath) + axios + .get(this.diffForPath) .then(({ data }) => { this.loadingContent.hide(); if (data.html) { diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index 5e385400747..8ca590123ae 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -93,7 +93,9 @@ export default class SmartInterval { destroy() { this.cancel(); document.removeEventListener('visibilitychange', this.handleVisibilityChange); - $(document).off('visibilitychange').off('beforeunload'); + $(document) + .off('visibilitychange') + .off('beforeunload'); } /* private */ @@ -111,11 +113,12 @@ export default class SmartInterval { triggerCallback() { this.isLoading = true; - this.cfg.callback() + this.cfg + .callback() .then(() => { this.isLoading = false; }) - .catch((err) => { + .catch(err => { this.isLoading = false; throw err; }); @@ -134,9 +137,9 @@ export default class SmartInterval { handleVisibilityChange(e) { this.state.pageVisibility = e.target.visibilityState; - const intervalAction = this.isPageVisible() ? - this.onVisibilityVisible : - this.onVisibilityHidden; + const intervalAction = this.isPageVisible() + ? this.onVisibilityVisible + : this.onVisibilityHidden; intervalAction.apply(this); } @@ -162,7 +165,9 @@ export default class SmartInterval { this.setCurrentInterval(nextInterval); } - isPageVisible() { return this.state.pageVisibility === 'visible'; } + isPageVisible() { + return this.state.pageVisibility === 'visible'; + } stopTimer() { const { state } = this; @@ -170,4 +175,3 @@ export default class SmartInterval { state.intervalId = window.clearInterval(state.intervalId); } } - diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index f5a7fdae5d7..007b83e1927 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -11,10 +11,14 @@ export default class Star { const $starSpan = $this.find('span'); const $startIcon = $this.find('svg'); - axios.post($this.data('endpoint')) + axios + .post($this.data('endpoint')) .then(({ data }) => { const isStarred = $starSpan.hasClass('starred'); - $this.parent().find('.star-count').text(data.star_count); + $this + .parent() + .find('.star-count') + .text(data.star_count); if (isStarred) { $starSpan.removeClass('starred').text(s__('StarProject|Star')); diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index 48782e63b9b..edefb3735d7 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -26,7 +26,11 @@ export default class TaskList { // Prevent duplicate event bindings this.disable(); $(`${this.selector} .js-task-list-container`).taskList('enable'); - $(document).on('tasklist:changed', `${this.selector} .js-task-list-container`, this.update.bind(this)); + $(document).on( + 'tasklist:changed', + `${this.selector} .js-task-list-container`, + this.update.bind(this), + ); } disable() { @@ -41,7 +45,8 @@ export default class TaskList { [this.fieldName]: $target.val(), }; - return axios.patch($target.data('updateUrl') || $('form.js-issuable-update').attr('action'), patchData) + return axios + .patch($target.data('updateUrl') || $('form.js-issuable-update').attr('action'), patchData) .then(({ data }) => this.onSuccess(data)) .catch(err => this.onError(err)); } diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index 74166313940..6065770e68d 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -31,12 +31,18 @@ export default class IssuableTemplateSelector extends TemplateSelector { requestFile(query) { this.startLoadingSpinner(); - Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => { - this.currentTemplate = currentTemplate; - this.stopLoadingSpinner(); - if (err) return; // Error handled by global AJAX error handler - this.setInputValueToTemplateContent(); - }); + Api.issueTemplate( + this.namespacePath, + this.projectPath, + query.name, + this.issuableType, + (err, currentTemplate) => { + this.currentTemplate = currentTemplate; + this.stopLoadingSpinner(); + if (err) return; // Error handled by global AJAX error handler + this.setInputValueToTemplateContent(); + }, + ); return; } diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js index 74c5bbe45a4..b24aa8a3a34 100644 --- a/app/assets/javascripts/terminal/terminal.js +++ b/app/assets/javascripts/terminal/terminal.js @@ -4,10 +4,14 @@ import * as fit from 'xterm/lib/addons/fit/fit'; export default class GLTerminal { constructor(options = {}) { - this.options = Object.assign({}, { - cursorBlink: true, - screenKeys: true, - }, options); + this.options = Object.assign( + {}, + { + cursorBlink: true, + screenKeys: true, + }, + options, + ); this.container = document.querySelector(options.selector); diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js index a5c18042ce7..be9ebc81c6b 100644 --- a/app/assets/javascripts/test_utils/simulate_drag.js +++ b/app/assets/javascripts/test_utils/simulate_drag.js @@ -4,15 +4,43 @@ function simulateEvent(el, type, options = {}) { if (/^mouse/.test(type)) { event = el.ownerDocument.createEvent('MouseEvents'); - event.initMouseEvent(type, true, true, el.ownerDocument.defaultView, - options.button, options.screenX, options.screenY, options.clientX, options.clientY, - options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); + event.initMouseEvent( + type, + true, + true, + el.ownerDocument.defaultView, + options.button, + options.screenX, + options.screenY, + options.clientX, + options.clientY, + options.ctrlKey, + options.altKey, + options.shiftKey, + options.metaKey, + options.button, + el, + ); } else { event = el.ownerDocument.createEvent('CustomEvent'); - event.initCustomEvent(type, true, true, el.ownerDocument.defaultView, - options.button, options.screenX, options.screenY, options.clientX, options.clientY, - options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); + event.initCustomEvent( + type, + true, + true, + el.ownerDocument.defaultView, + options.button, + options.screenX, + options.screenY, + options.clientX, + options.clientY, + options.ctrlKey, + options.altKey, + options.shiftKey, + options.metaKey, + options.button, + el, + ); event.dataTransfer = { data: {}, @@ -37,14 +65,16 @@ function simulateEvent(el, type, options = {}) { } function isLast(target) { - const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; + const el = + typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; const { children } = el; return children.length - 1 === target.index; } function getTarget(target) { - const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; + const el = + typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; const { children } = el; return ( @@ -58,13 +88,13 @@ function getTarget(target) { function getRect(el) { const rect = el.getBoundingClientRect(); const width = rect.right - rect.left; - const height = (rect.bottom - rect.top) + 10; + const height = rect.bottom - rect.top + 10; return { x: rect.left, y: rect.top, - cx: rect.left + (width / 2), - cy: rect.top + (height / 2), + cx: rect.left + width / 2, + cy: rect.top + height / 2, w: width, h: height, hw: width / 2, @@ -112,8 +142,8 @@ export default function simulateDrag(options) { const dragInterval = setInterval(() => { const progress = (new Date().getTime() - startTime) / duration; - const x = (fromRect.cx + ((toRect.cx - fromRect.cx) * progress)); - const y = (fromRect.cy + ((toRect.cy - fromRect.cy) * progress)); + const x = fromRect.cx + (toRect.cx - fromRect.cx) * progress; + const y = fromRect.cy + (toRect.cy - fromRect.cy) * progress; const overEl = fromEl.ownerDocument.elementFromPoint(x, y); simulateEvent(overEl, 'mousemove', { diff --git a/app/assets/javascripts/test_utils/simulate_input.js b/app/assets/javascripts/test_utils/simulate_input.js index 90c1b7cb57e..c300c806e6d 100644 --- a/app/assets/javascripts/test_utils/simulate_input.js +++ b/app/assets/javascripts/test_utils/simulate_input.js @@ -12,7 +12,7 @@ export default function simulateInput(target, text) { } if (text.length > 0) { - Array.prototype.forEach.call(text, (char) => { + Array.prototype.forEach.call(text, char => { input.value += char; triggerEvents(input); }); diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js index 199b14458ed..d83ffc7e211 100644 --- a/app/assets/javascripts/toggle_buttons.js +++ b/app/assets/javascripts/toggle_buttons.js @@ -49,7 +49,7 @@ function onToggleClicked(toggle, input, clickCallback) { export default function setupToggleButtons(container, clickCallback = () => {}) { const toggles = container.querySelectorAll('.js-project-feature-toggle'); - toggles.forEach((toggle) => { + toggles.forEach(toggle => { const input = toggle.querySelector('.js-project-feature-toggle-input'); const isOn = convertPermissionToBoolean(input.value); diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 066fd6278a7..3e659c9e7ea 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -8,7 +8,7 @@ export default class TreeView { this.initKeyNav(); // Code browser tree slider // Make the entire tree-item row clickable, but not if clicking another link (like a commit message) - $(".tree-content-holder .tree-item").on('click', function(e) { + $('.tree-content-holder .tree-item').on('click', function(e) { var $clickedEl, path; $clickedEl = $(e.target); path = $('.tree-item-file-name a', this).attr('href'); @@ -27,33 +27,33 @@ export default class TreeView { initKeyNav() { var li, liSelected; - li = $("tr.tree-item"); + li = $('tr.tree-item'); liSelected = null; return $('body').keydown(function(e) { var next, path; - if ($("input:focus").length > 0 && (e.which === 38 || e.which === 40)) { + if ($('input:focus').length > 0 && (e.which === 38 || e.which === 40)) { return false; } if (e.which === 40) { if (liSelected) { next = liSelected.next(); if (next.length > 0) { - liSelected.removeClass("selected"); - liSelected = next.addClass("selected"); + liSelected.removeClass('selected'); + liSelected = next.addClass('selected'); } } else { - liSelected = li.eq(0).addClass("selected"); + liSelected = li.eq(0).addClass('selected'); } return $(liSelected).focus(); } else if (e.which === 38) { if (liSelected) { next = liSelected.prev(); if (next.length > 0) { - liSelected.removeClass("selected"); - liSelected = next.addClass("selected"); + liSelected.removeClass('selected'); + liSelected = next.addClass('selected'); } } else { - liSelected = li.last().addClass("selected"); + liSelected = li.last().addClass('selected'); } return $(liSelected).focus(); } else if (e.which === 13) { diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index 78fd7ad441f..abfc81e681e 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -49,7 +49,7 @@ export default class U2FAuthenticate { start() { return importU2FLibrary() - .then((utils) => { + .then(utils => { this.u2fUtils = utils; this.renderInProgress(); }) @@ -57,14 +57,19 @@ export default class U2FAuthenticate { } authenticate() { - return this.u2fUtils.sign(this.appId, this.challenge, this.signRequests, - (response) => { + return this.u2fUtils.sign( + this.appId, + this.challenge, + this.signRequests, + response => { if (response.errorCode) { const error = new U2FError(response.errorCode, 'authenticate'); return this.renderError(error); } return this.renderAuthenticated(JSON.stringify(response)); - }, 10); + }, + 10, + ); } renderTemplate(name, params) { @@ -99,5 +104,4 @@ export default class U2FAuthenticate { this.container[0].classList.add('hidden'); this.fallbackUI.classList.remove('hidden'); } - } diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 01e259a741d..43c814c8070 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -34,7 +34,7 @@ export default class U2FRegister { start() { return importU2FLibrary() - .then((utils) => { + .then(utils => { this.u2fUtils = utils; this.renderSetup(); }) @@ -42,14 +42,19 @@ export default class U2FRegister { } register() { - return this.u2fUtils.register(this.appId, this.registerRequests, this.signRequests, - (response) => { + return this.u2fUtils.register( + this.appId, + this.registerRequests, + this.signRequests, + response => { if (response.errorCode) { const error = new U2FError(response.errorCode, 'register'); return this.renderError(error); } return this.renderRegistered(JSON.stringify(response)); - }, 10); + }, + 10, + ); } renderTemplate(name, params) { diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js index 5778f00332d..b706481c02f 100644 --- a/app/assets/javascripts/u2f/util.js +++ b/app/assets/javascripts/u2f/util.js @@ -19,11 +19,10 @@ function getChromeVersion(userAgent) { export function canInjectU2fApi(userAgent) { const isSupportedChrome = isChrome(userAgent) && getChromeVersion(userAgent) >= 41; const isSupportedOpera = isOpera(userAgent) && getOperaVersion(userAgent) >= 40; - const isMobile = ( + const isMobile = userAgent.indexOf('droid') >= 0 || userAgent.indexOf('CriOS') >= 0 || - /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) - ); + /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent); return (isSupportedChrome || isSupportedOpera) && !isMobile; } diff --git a/app/assets/javascripts/ui_development_kit.js b/app/assets/javascripts/ui_development_kit.js index 9b242ea779d..be18ac5da24 100644 --- a/app/assets/javascripts/ui_development_kit.js +++ b/app/assets/javascripts/ui_development_kit.js @@ -4,13 +4,17 @@ import Api from './api'; export default () => { $('#js-project-dropdown').glDropdown({ data: (term, callback) => { - Api.projects(term, { - order_by: 'last_activity_at', - }, (data) => { - callback(data); - }); + Api.projects( + term, + { + order_by: 'last_activity_at', + }, + data => { + callback(data); + }, + ); }, - text: project => (project.name_with_namespace || project.name), + text: project => project.name_with_namespace || project.name, selectable: true, fieldName: 'author_id', filterable: true, @@ -18,6 +22,6 @@ export default () => { fields: ['name_with_namespace'], }, id: data => data.id, - isSelected: data => (data.id === 2), + isSelected: data => data.id === 2, }); }; diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/usage_ping_consent.js index ae3fde190e3..05607f09a7e 100644 --- a/app/assets/javascripts/usage_ping_consent.js +++ b/app/assets/javascripts/usage_ping_consent.js @@ -4,7 +4,7 @@ import Flash, { hideFlash } from './flash'; import { convertPermissionToBoolean } from './lib/utils/common_utils'; export default () => { - $('body').on('click', '.js-usage-consent-action', (e) => { + $('body').on('click', '.js-usage-consent-action', e => { e.preventDefault(); e.stopImmediatePropagation(); // overwrite rails listener @@ -18,7 +18,8 @@ export default () => { const hideConsentMessage = () => hideFlash(document.querySelector('.ping-consent-message')); - axios.put(url, data) + axios + .put(url, data) .then(() => { hideConsentMessage(); }) diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index e2259a8d07b..4b090212d83 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -15,8 +15,8 @@ function UsersSelect(currentUser, els, options = {}) { var $els; this.users = this.users.bind(this); this.user = this.user.bind(this); - this.usersPath = "/autocomplete/users.json"; - this.userPath = "/autocomplete/users/:id.json"; + this.usersPath = '/autocomplete/users.json'; + this.userPath = '/autocomplete/users/:id.json'; if (currentUser != null) { if (typeof currentUser === 'object') { this.currentUser = currentUser; @@ -33,156 +33,180 @@ function UsersSelect(currentUser, els, options = {}) { $els = $('.js-user-search'); } - $els.each((function(_this) { - return function(i, dropdown) { - var options = {}; - var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove; - $dropdown = $(dropdown); - options.projectId = $dropdown.data('projectId'); - options.groupId = $dropdown.data('groupId'); - options.showCurrentUser = $dropdown.data('currentUser'); - options.todoFilter = $dropdown.data('todoFilter'); - options.todoStateFilter = $dropdown.data('todoStateFilter'); - showNullUser = $dropdown.data('nullUser'); - defaultNullUser = $dropdown.data('nullUserDefault'); - showMenuAbove = $dropdown.data('showMenuAbove'); - showAnyUser = $dropdown.data('anyUser'); - firstUser = $dropdown.data('firstUser'); - options.authorId = $dropdown.data('authorId'); - defaultLabel = $dropdown.data('defaultLabel'); - issueURL = $dropdown.data('issueUpdate'); - $selectbox = $dropdown.closest('.selectbox'); - $block = $selectbox.closest('.block'); - abilityName = $dropdown.data('abilityName'); - $value = $block.find('.value'); - $collapsedSidebar = $block.find('.sidebar-collapsed-user'); - $loading = $block.find('.block-loading').fadeOut(); - selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null; - selectedId = $dropdown.data('selected'); - - if (selectedId === undefined) { - selectedId = selectedIdDefault; - } - - const assignYourself = function () { - const unassignedSelected = $dropdown.closest('.selectbox') - .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`); - - if (unassignedSelected) { - unassignedSelected.remove(); + $els.each( + (function(_this) { + return function(i, dropdown) { + var options = {}; + var $block, + $collapsedSidebar, + $dropdown, + $loading, + $selectbox, + $value, + abilityName, + assignTo, + assigneeTemplate, + collapsedAssigneeTemplate, + defaultLabel, + defaultNullUser, + firstUser, + issueURL, + selectedId, + selectedIdDefault, + showAnyUser, + showNullUser, + showMenuAbove; + $dropdown = $(dropdown); + options.projectId = $dropdown.data('projectId'); + options.groupId = $dropdown.data('groupId'); + options.showCurrentUser = $dropdown.data('currentUser'); + options.todoFilter = $dropdown.data('todoFilter'); + options.todoStateFilter = $dropdown.data('todoStateFilter'); + showNullUser = $dropdown.data('nullUser'); + defaultNullUser = $dropdown.data('nullUserDefault'); + showMenuAbove = $dropdown.data('showMenuAbove'); + showAnyUser = $dropdown.data('anyUser'); + firstUser = $dropdown.data('firstUser'); + options.authorId = $dropdown.data('authorId'); + defaultLabel = $dropdown.data('defaultLabel'); + issueURL = $dropdown.data('issueUpdate'); + $selectbox = $dropdown.closest('.selectbox'); + $block = $selectbox.closest('.block'); + abilityName = $dropdown.data('abilityName'); + $value = $block.find('.value'); + $collapsedSidebar = $block.find('.sidebar-collapsed-user'); + $loading = $block.find('.block-loading').fadeOut(); + selectedIdDefault = defaultNullUser && showNullUser ? 0 : null; + selectedId = $dropdown.data('selected'); + + if (selectedId === undefined) { + selectedId = selectedIdDefault; } - // Save current selected user to the DOM - const input = document.createElement('input'); - input.type = 'hidden'; - input.name = $dropdown.data('fieldName'); + const assignYourself = function() { + const unassignedSelected = $dropdown + .closest('.selectbox') + .find(`input[name='${$dropdown.data('fieldName')}'][value=0]`); - const currentUserInfo = $dropdown.data('currentUserInfo'); - - if (currentUserInfo) { - input.value = currentUserInfo.id; - input.dataset.meta = _.escape(currentUserInfo.name); - } else if (_this.currentUser) { - input.value = _this.currentUser.id; - } - - if ($selectbox) { - $dropdown.parent().before(input); - } else { - $dropdown.after(input); - } - }; - - if ($block[0]) { - $block[0].addEventListener('assignYourself', assignYourself); - } - - const getSelectedUserInputs = function() { - return $selectbox - .find(`input[name="${$dropdown.data('fieldName')}"]`); - }; + if (unassignedSelected) { + unassignedSelected.remove(); + } - const getSelected = function() { - return getSelectedUserInputs() - .map((index, input) => parseInt(input.value, 10)) - .get(); - }; + // Save current selected user to the DOM + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = $dropdown.data('fieldName'); - const checkMaxSelect = function() { - const maxSelect = $dropdown.data('maxSelect'); - if (maxSelect) { - const selected = getSelected(); + const currentUserInfo = $dropdown.data('currentUserInfo'); - if (selected.length > maxSelect) { - const firstSelectedId = selected[0]; - const firstSelected = $dropdown.closest('.selectbox') - .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`); + if (currentUserInfo) { + input.value = currentUserInfo.id; + input.dataset.meta = _.escape(currentUserInfo.name); + } else if (_this.currentUser) { + input.value = _this.currentUser.id; + } - firstSelected.remove(); - emitSidebarEvent('sidebar.removeAssignee', { - id: firstSelectedId, - }); + if ($selectbox) { + $dropdown.parent().before(input); + } else { + $dropdown.after(input); } - } - }; + }; - const getMultiSelectDropdownTitle = function(selectedUser, isSelected) { - const selectedUsers = getSelected() - .filter(u => u !== 0); - - const firstUser = getSelectedUserInputs() - .map((index, input) => ({ - name: input.dataset.meta, - value: parseInt(input.value, 10), - })) - .filter(u => u.id !== 0) - .get(0); - - if (selectedUsers.length === 0) { - return 'Unassigned'; - } else if (selectedUsers.length === 1) { - return firstUser.name; - } else if (isSelected) { - const otherSelected = selectedUsers.filter(s => s !== selectedUser.id); - return `${selectedUser.name} + ${otherSelected.length} more`; - } else { - return `${firstUser.name} + ${selectedUsers.length - 1} more`; + if ($block[0]) { + $block[0].addEventListener('assignYourself', assignYourself); } - }; - $('.assign-to-me-link').on('click', (e) => { - e.preventDefault(); - $(e.currentTarget).hide(); + const getSelectedUserInputs = function() { + return $selectbox.find(`input[name="${$dropdown.data('fieldName')}"]`); + }; + + const getSelected = function() { + return getSelectedUserInputs() + .map((index, input) => parseInt(input.value, 10)) + .get(); + }; + + const checkMaxSelect = function() { + const maxSelect = $dropdown.data('maxSelect'); + if (maxSelect) { + const selected = getSelected(); + + if (selected.length > maxSelect) { + const firstSelectedId = selected[0]; + const firstSelected = $dropdown + .closest('.selectbox') + .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`); + + firstSelected.remove(); + emitSidebarEvent('sidebar.removeAssignee', { + id: firstSelectedId, + }); + } + } + }; + + const getMultiSelectDropdownTitle = function(selectedUser, isSelected) { + const selectedUsers = getSelected().filter(u => u !== 0); + + const firstUser = getSelectedUserInputs() + .map((index, input) => ({ + name: input.dataset.meta, + value: parseInt(input.value, 10), + })) + .filter(u => u.id !== 0) + .get(0); + + if (selectedUsers.length === 0) { + return 'Unassigned'; + } else if (selectedUsers.length === 1) { + return firstUser.name; + } else if (isSelected) { + const otherSelected = selectedUsers.filter(s => s !== selectedUser.id); + return `${selectedUser.name} + ${otherSelected.length} more`; + } else { + return `${firstUser.name} + ${selectedUsers.length - 1} more`; + } + }; - if ($dropdown.data('multiSelect')) { - assignYourself(); - checkMaxSelect(); + $('.assign-to-me-link').on('click', e => { + e.preventDefault(); + $(e.currentTarget).hide(); - const currentUserInfo = $dropdown.data('currentUserInfo'); - $dropdown.find('.dropdown-toggle-text').text(getMultiSelectDropdownTitle(currentUserInfo)).removeClass('is-default'); - } else { - const $input = $(`input[name="${$dropdown.data('fieldName')}"]`); - $input.val(gon.current_user_id); - selectedId = $input.val(); - $dropdown.find('.dropdown-toggle-text').text(gon.current_user_fullname).removeClass('is-default'); - } - }); - - $block.on('click', '.js-assign-yourself', (e) => { - e.preventDefault(); - return assignTo(_this.currentUser.id); - }); - - assignTo = function(selected) { - var data; - data = {}; - data[abilityName] = {}; - data[abilityName].assignee_id = selected != null ? selected : null; - $loading.removeClass('hidden').fadeIn(); - $dropdown.trigger('loading.gl.dropdown'); - - return axios.put(issueURL, data) - .then(({ data }) => { + if ($dropdown.data('multiSelect')) { + assignYourself(); + checkMaxSelect(); + + const currentUserInfo = $dropdown.data('currentUserInfo'); + $dropdown + .find('.dropdown-toggle-text') + .text(getMultiSelectDropdownTitle(currentUserInfo)) + .removeClass('is-default'); + } else { + const $input = $(`input[name="${$dropdown.data('fieldName')}"]`); + $input.val(gon.current_user_id); + selectedId = $input.val(); + $dropdown + .find('.dropdown-toggle-text') + .text(gon.current_user_fullname) + .removeClass('is-default'); + } + }); + + $block.on('click', '.js-assign-yourself', e => { + e.preventDefault(); + return assignTo(_this.currentUser.id); + }); + + assignTo = function(selected) { + var data; + data = {}; + data[abilityName] = {}; + data[abilityName].assignee_id = selected != null ? selected : null; + $loading.removeClass('hidden').fadeIn(); + $dropdown.trigger('loading.gl.dropdown'); + + return axios.put(issueURL, data).then(({ data }) => { var user, tooltipTitle; $dropdown.trigger('loaded.gl.dropdown'); $loading.fadeOut(); @@ -190,14 +214,14 @@ function UsersSelect(currentUser, els, options = {}) { user = { name: data.assignee.name, username: data.assignee.username, - avatar: data.assignee.avatar_url + avatar: data.assignee.avatar_url, }; tooltipTitle = _.escape(user.name); } else { user = { name: 'Unassigned', username: '', - avatar: '' + avatar: '', }; tooltipTitle = __('Assignee'); } @@ -205,319 +229,341 @@ function UsersSelect(currentUser, els, options = {}) { $collapsedSidebar.attr('title', tooltipTitle).tooltip('_fixTitle'); return $collapsedSidebar.html(collapsedAssigneeTemplate(user)); }); - }; - collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>'); - assigneeTemplate = _.template('<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>'); - return $dropdown.glDropdown({ - showMenuAbove: showMenuAbove, - data: function(term, callback) { - return _this.users(term, options, function(users) { - // GitLabDropdownFilter returns this.instance - // GitLabDropdownRemote returns this.options.instance - const glDropdown = this.instance || this.options.instance; - glDropdown.options.processData(term, users, callback); - }.bind(this)); - }, - processData: function(term, data, callback) { - let users = data; - - // Only show assigned user list when there is no search term - if ($dropdown.hasClass('js-multiselect') && term.length === 0) { - const selectedInputs = getSelectedUserInputs(); - - // Potential duplicate entries when dealing with issue board - // because issue board is also managed by vue - const selectedUsers = _.uniq(selectedInputs, false, a => a.value) - .filter((input) => { - const userId = parseInt(input.value, 10); - const inUsersArray = users.find(u => u.id === userId); - - return !inUsersArray && userId !== 0; - }) - .map((input) => { - const userId = parseInt(input.value, 10); - const { avatarUrl, avatar_url, name, username } = input.dataset; - return { - avatar_url: avatarUrl || avatar_url, - id: userId, - name, - username, - }; - }); + }; + collapsedAssigneeTemplate = _.template( + '<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>', + ); + assigneeTemplate = _.template( + '<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>', + ); + return $dropdown.glDropdown({ + showMenuAbove: showMenuAbove, + data: function(term, callback) { + return _this.users( + term, + options, + function(users) { + // GitLabDropdownFilter returns this.instance + // GitLabDropdownRemote returns this.options.instance + const glDropdown = this.instance || this.options.instance; + glDropdown.options.processData(term, users, callback); + }.bind(this), + ); + }, + processData: function(term, data, callback) { + let users = data; + + // Only show assigned user list when there is no search term + if ($dropdown.hasClass('js-multiselect') && term.length === 0) { + const selectedInputs = getSelectedUserInputs(); + + // Potential duplicate entries when dealing with issue board + // because issue board is also managed by vue + const selectedUsers = _.uniq(selectedInputs, false, a => a.value) + .filter(input => { + const userId = parseInt(input.value, 10); + const inUsersArray = users.find(u => u.id === userId); + + return !inUsersArray && userId !== 0; + }) + .map(input => { + const userId = parseInt(input.value, 10); + const { avatarUrl, avatar_url, name, username } = input.dataset; + return { + avatar_url: avatarUrl || avatar_url, + id: userId, + name, + username, + }; + }); - users = data.concat(selectedUsers); - } + users = data.concat(selectedUsers); + } - let anyUser; - let index; - let len; - let name; - let obj; - let showDivider; - if (term.length === 0) { - showDivider = 0; - if (firstUser) { - // Move current user to the front of the list - for (index = 0, len = users.length; index < len; index += 1) { - obj = users[index]; - if (obj.username === firstUser) { - users.splice(index, 1); - users.unshift(obj); - break; + let anyUser; + let index; + let len; + let name; + let obj; + let showDivider; + if (term.length === 0) { + showDivider = 0; + if (firstUser) { + // Move current user to the front of the list + for (index = 0, len = users.length; index < len; index += 1) { + obj = users[index]; + if (obj.username === firstUser) { + users.splice(index, 1); + users.unshift(obj); + break; + } } } - } - if (showNullUser) { - showDivider += 1; - users.unshift({ - beforeDivider: true, - name: 'Unassigned', - id: 0 - }); - } - if (showAnyUser) { - showDivider += 1; - name = showAnyUser; - if (name === true) { - name = 'Any User'; + if (showNullUser) { + showDivider += 1; + users.unshift({ + beforeDivider: true, + name: 'Unassigned', + id: 0, + }); + } + if (showAnyUser) { + showDivider += 1; + name = showAnyUser; + if (name === true) { + name = 'Any User'; + } + anyUser = { + beforeDivider: true, + name: name, + id: null, + }; + users.unshift(anyUser); } - anyUser = { - beforeDivider: true, - name: name, - id: null - }; - users.unshift(anyUser); - } - if (showDivider) { - users.splice(showDivider, 0, 'divider'); - } + if (showDivider) { + users.splice(showDivider, 0, 'divider'); + } - if ($dropdown.hasClass('js-multiselect')) { - const selected = getSelected().filter(i => i !== 0); + if ($dropdown.hasClass('js-multiselect')) { + const selected = getSelected().filter(i => i !== 0); - if (selected.length > 0) { - if ($dropdown.data('dropdownHeader')) { - showDivider += 1; - users.splice(showDivider, 0, { - header: $dropdown.data('dropdownHeader'), - }); - } + if (selected.length > 0) { + if ($dropdown.data('dropdownHeader')) { + showDivider += 1; + users.splice(showDivider, 0, { + header: $dropdown.data('dropdownHeader'), + }); + } - const selectedUsers = users - .filter(u => selected.indexOf(u.id) !== -1) - .sort((a, b) => a.name > b.name); + const selectedUsers = users + .filter(u => selected.indexOf(u.id) !== -1) + .sort((a, b) => a.name > b.name); - users = users.filter(u => selected.indexOf(u.id) === -1); + users = users.filter(u => selected.indexOf(u.id) === -1); - selectedUsers.forEach((selectedUser) => { - showDivider += 1; - users.splice(showDivider, 0, selectedUser); - }); + selectedUsers.forEach(selectedUser => { + showDivider += 1; + users.splice(showDivider, 0, selectedUser); + }); - users.splice(showDivider + 1, 0, 'divider'); + users.splice(showDivider + 1, 0, 'divider'); + } } } - } - callback(users); - if (showMenuAbove) { - $dropdown.data('glDropdown').positionMenuAbove(); - } - }, - filterable: true, - filterRemote: true, - search: { - fields: ['name', 'username'] - }, - selectable: true, - fieldName: $dropdown.data('fieldName'), - toggleLabel: function(selected, el, glDropdown) { - const inputValue = glDropdown.filterInput.val(); - - if (this.multiSelect && inputValue === '') { - // Remove non-users from the fullData array - const users = glDropdown.filteredFullData(); - const callback = glDropdown.parseData.bind(glDropdown); - - // Update the data model - this.processData(inputValue, users, callback); - } + callback(users); + if (showMenuAbove) { + $dropdown.data('glDropdown').positionMenuAbove(); + } + }, + filterable: true, + filterRemote: true, + search: { + fields: ['name', 'username'], + }, + selectable: true, + fieldName: $dropdown.data('fieldName'), + toggleLabel: function(selected, el, glDropdown) { + const inputValue = glDropdown.filterInput.val(); + + if (this.multiSelect && inputValue === '') { + // Remove non-users from the fullData array + const users = glDropdown.filteredFullData(); + const callback = glDropdown.parseData.bind(glDropdown); + + // Update the data model + this.processData(inputValue, users, callback); + } - if (this.multiSelect) { - return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active')); - } + if (this.multiSelect) { + return getMultiSelectDropdownTitle(selected, $(el).hasClass('is-active')); + } - if (selected && 'id' in selected && $(el).hasClass('is-active')) { - $dropdown.find('.dropdown-toggle-text').removeClass('is-default'); - if (selected.text) { - return selected.text; + if (selected && 'id' in selected && $(el).hasClass('is-active')) { + $dropdown.find('.dropdown-toggle-text').removeClass('is-default'); + if (selected.text) { + return selected.text; + } else { + return selected.name; + } } else { - return selected.name; + $dropdown.find('.dropdown-toggle-text').addClass('is-default'); + return defaultLabel; + } + }, + defaultLabel: defaultLabel, + hidden: function(e) { + if ($dropdown.hasClass('js-multiselect')) { + emitSidebarEvent('sidebar.saveAssignees'); } - } else { - $dropdown.find('.dropdown-toggle-text').addClass('is-default'); - return defaultLabel; - } - }, - defaultLabel: defaultLabel, - hidden: function(e) { - if ($dropdown.hasClass('js-multiselect')) { - emitSidebarEvent('sidebar.saveAssignees'); - } - - if (!$dropdown.data('alwaysShowSelectbox')) { - $selectbox.hide(); - // Recalculate where .value is because vue might have changed it - $block = $selectbox.closest('.block'); - $value = $block.find('.value'); - // display:block overrides the hide-collapse rule - $value.css('display', ''); - } - }, - multiSelect: $dropdown.hasClass('js-multiselect'), - inputMeta: $dropdown.data('inputMeta'), - clicked: function(options) { - const { $el, e, isMarking } = options; - const user = options.selectedObj; - - if ($dropdown.hasClass('js-multiselect')) { - const isActive = $el.hasClass('is-active'); - const previouslySelected = $dropdown.closest('.selectbox') - .find("input[name='" + ($dropdown.data('fieldName')) + "'][value!=0]"); - - // Enables support for limiting the number of users selected - // Automatically removes the first on the list if more users are selected - checkMaxSelect(); + if (!$dropdown.data('alwaysShowSelectbox')) { + $selectbox.hide(); - if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') { - // Unassigned selected - previouslySelected.each((index, element) => { - const id = parseInt(element.value, 10); - element.remove(); - }); - emitSidebarEvent('sidebar.removeAllAssignees'); - } else if (isActive) { - // user selected - emitSidebarEvent('sidebar.addAssignee', user); + // Recalculate where .value is because vue might have changed it + $block = $selectbox.closest('.block'); + $value = $block.find('.value'); + // display:block overrides the hide-collapse rule + $value.css('display', ''); + } + }, + multiSelect: $dropdown.hasClass('js-multiselect'), + inputMeta: $dropdown.data('inputMeta'), + clicked: function(options) { + const { $el, e, isMarking } = options; + const user = options.selectedObj; - // Remove unassigned selection (if it was previously selected) - const unassignedSelected = $dropdown.closest('.selectbox') - .find("input[name='" + ($dropdown.data('fieldName')) + "'][value=0]"); + if ($dropdown.hasClass('js-multiselect')) { + const isActive = $el.hasClass('is-active'); + const previouslySelected = $dropdown + .closest('.selectbox') + .find("input[name='" + $dropdown.data('fieldName') + "'][value!=0]"); + + // Enables support for limiting the number of users selected + // Automatically removes the first on the list if more users are selected + checkMaxSelect(); + + if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') { + // Unassigned selected + previouslySelected.each((index, element) => { + const id = parseInt(element.value, 10); + element.remove(); + }); + emitSidebarEvent('sidebar.removeAllAssignees'); + } else if (isActive) { + // user selected + emitSidebarEvent('sidebar.addAssignee', user); + + // Remove unassigned selection (if it was previously selected) + const unassignedSelected = $dropdown + .closest('.selectbox') + .find("input[name='" + $dropdown.data('fieldName') + "'][value=0]"); + + if (unassignedSelected) { + unassignedSelected.remove(); + } + } else { + if (previouslySelected.length === 0) { + // Select unassigned because there is no more selected users + this.addInput($dropdown.data('fieldName'), 0, {}); + } - if (unassignedSelected) { - unassignedSelected.remove(); - } - } else { - if (previouslySelected.length === 0) { - // Select unassigned because there is no more selected users - this.addInput($dropdown.data('fieldName'), 0, {}); + // User unselected + emitSidebarEvent('sidebar.removeAssignee', user); } - // User unselected - emitSidebarEvent('sidebar.removeAssignee', user); + if (getSelected().find(u => u === gon.current_user_id)) { + $('.assign-to-me-link').hide(); + } else { + $('.assign-to-me-link').show(); + } } - if (getSelected().find(u => u === gon.current_user_id)) { - $('.assign-to-me-link').hide(); - } else { - $('.assign-to-me-link').show(); + var isIssueIndex, isMRIndex, page, selected; + page = $('body').attr('data-page'); + isIssueIndex = page === 'projects:issues:index'; + isMRIndex = page === page && page === 'projects:merge_requests:index'; + if ( + $dropdown.hasClass('js-filter-bulk-update') || + $dropdown.hasClass('js-issuable-form-dropdown') + ) { + e.preventDefault(); + + const isSelecting = user.id !== selectedId; + selectedId = isSelecting ? user.id : selectedIdDefault; + + if (selectedId === gon.current_user_id) { + $('.assign-to-me-link').hide(); + } else { + $('.assign-to-me-link').show(); + } + return; + } + if ($el.closest('.add-issues-modal').length) { + ModalStore.store.filter[$dropdown.data('fieldName')] = user.id; + } else if (handleClick) { + e.preventDefault(); + handleClick(user, isMarking); + } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { + return Issuable.filterResults($dropdown.closest('form')); + } else if ($dropdown.hasClass('js-filter-submit')) { + return $dropdown.closest('form').submit(); + } else if (!$dropdown.hasClass('js-multiselect')) { + selected = $dropdown + .closest('.selectbox') + .find("input[name='" + $dropdown.data('fieldName') + "']") + .val(); + return assignTo(selected); } - } - var isIssueIndex, isMRIndex, page, selected; - page = $('body').attr('data-page'); - isIssueIndex = page === 'projects:issues:index'; - isMRIndex = (page === page && page === 'projects:merge_requests:index'); - if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { - e.preventDefault(); + // Automatically close dropdown after assignee is selected + // since CE has no multiple assignees + // EE does not have a max-select + if ( + $dropdown.data('maxSelect') && + getSelected().length === $dropdown.data('maxSelect') + ) { + // Close the dropdown + $dropdown.dropdown('toggle'); + } + }, + id: function(user) { + return user.id; + }, + opened: function(e) { + const $el = $(e.currentTarget); + const selected = getSelected(); + if ($dropdown.hasClass('js-issue-board-sidebar') && selected.length === 0) { + this.addInput($dropdown.data('fieldName'), 0, {}); + } + $el.find('.is-active').removeClass('is-active'); - const isSelecting = (user.id !== selectedId); - selectedId = isSelecting ? user.id : selectedIdDefault; + function highlightSelected(id) { + $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active'); + } - if (selectedId === gon.current_user_id) { - $('.assign-to-me-link').hide(); + if (selected.length > 0) { + getSelected().forEach(selectedId => highlightSelected(selectedId)); + } else if ($dropdown.hasClass('js-issue-board-sidebar')) { + highlightSelected(0); } else { - $('.assign-to-me-link').show(); + highlightSelected(selectedId); } - return; - } - if ($el.closest('.add-issues-modal').length) { - ModalStore.store.filter[$dropdown.data('fieldName')] = user.id; - } else if (handleClick) { - e.preventDefault(); - handleClick(user, isMarking); - } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { - return Issuable.filterResults($dropdown.closest('form')); - } else if ($dropdown.hasClass('js-filter-submit')) { - return $dropdown.closest('form').submit(); - } else if (!$dropdown.hasClass('js-multiselect')) { - selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('fieldName')) + "']").val(); - return assignTo(selected); - } - - // Automatically close dropdown after assignee is selected - // since CE has no multiple assignees - // EE does not have a max-select - if ($dropdown.data('maxSelect') && - getSelected().length === $dropdown.data('maxSelect')) { - // Close the dropdown - $dropdown.dropdown('toggle'); - } - }, - id: function (user) { - return user.id; - }, - opened: function(e) { - const $el = $(e.currentTarget); - const selected = getSelected(); - if ($dropdown.hasClass('js-issue-board-sidebar') && selected.length === 0) { - this.addInput($dropdown.data('fieldName'), 0, {}); - } - $el.find('.is-active').removeClass('is-active'); - - function highlightSelected(id) { - $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active'); - } + }, + updateLabel: $dropdown.data('dropdownTitle'), + renderRow: function(user) { + var avatar, img, listClosingTags, listWithName, listWithUserName, username; + username = user.username ? '@' + user.username : ''; + avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url; - if (selected.length > 0) { - getSelected().forEach(selectedId => highlightSelected(selectedId)); - } else if ($dropdown.hasClass('js-issue-board-sidebar')) { - highlightSelected(0); - } else { - highlightSelected(selectedId); - } - }, - updateLabel: $dropdown.data('dropdownTitle'), - renderRow: function(user) { - var avatar, img, listClosingTags, listWithName, listWithUserName, username; - username = user.username ? "@" + user.username : ""; - avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url; + let selected = false; - let selected = false; + if (this.multiSelect) { + selected = getSelected().find(u => user.id === u); - if (this.multiSelect) { - selected = getSelected().find(u => user.id === u); + const { fieldName } = this; + const field = $dropdown + .closest('.selectbox') + .find("input[name='" + fieldName + "'][value='" + user.id + "']"); - const { fieldName } = this; - const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']"); - - if (field.length) { - selected = true; + if (field.length) { + selected = true; + } + } else { + selected = user.id === selectedId; } - } else { - selected = user.id === selectedId; - } - img = ""; - if (user.beforeDivider != null) { - `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`; - } else { - img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; - } + img = ''; + if (user.beforeDivider != null) { + `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape( + user.name, + )}</a></li>`; + } else { + img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; + } - return ` + return ` <li data-user-id=${user.id}> <a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'> ${img} @@ -528,114 +574,117 @@ function UsersSelect(currentUser, els, options = {}) { </a> </li> `; - } - }); - }; - })(this)); - $('.ajax-users-select').each((function(_this) { - return function(i, select) { - var firstUser, showAnyUser, showEmailUser, showNullUser; - var options = {}; - options.skipLdap = $(select).hasClass('skip_ldap'); - options.projectId = $(select).data('projectId'); - options.groupId = $(select).data('groupId'); - options.showCurrentUser = $(select).data('currentUser'); - options.authorId = $(select).data('authorId'); - options.skipUsers = $(select).data('skipUsers'); - showNullUser = $(select).data('nullUser'); - showAnyUser = $(select).data('anyUser'); - showEmailUser = $(select).data('emailUser'); - firstUser = $(select).data('firstUser'); - return $(select).select2({ - placeholder: "Search for a user", - multiple: $(select).hasClass('multiselect'), - minimumInputLength: 0, - query: function(query) { - return _this.users(query.term, options, function(users) { - var anyUser, data, emailUser, index, len, name, nullUser, obj, ref; - data = { - results: users - }; - if (query.term.length === 0) { - if (firstUser) { - // Move current user to the front of the list - ref = data.results; - - for (index = 0, len = ref.length; index < len; index += 1) { - obj = ref[index]; - if (obj.username === firstUser) { - data.results.splice(index, 1); - data.results.unshift(obj); - break; + }, + }); + }; + })(this), + ); + $('.ajax-users-select').each( + (function(_this) { + return function(i, select) { + var firstUser, showAnyUser, showEmailUser, showNullUser; + var options = {}; + options.skipLdap = $(select).hasClass('skip_ldap'); + options.projectId = $(select).data('projectId'); + options.groupId = $(select).data('groupId'); + options.showCurrentUser = $(select).data('currentUser'); + options.authorId = $(select).data('authorId'); + options.skipUsers = $(select).data('skipUsers'); + showNullUser = $(select).data('nullUser'); + showAnyUser = $(select).data('anyUser'); + showEmailUser = $(select).data('emailUser'); + firstUser = $(select).data('firstUser'); + return $(select).select2({ + placeholder: 'Search for a user', + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query: function(query) { + return _this.users(query.term, options, function(users) { + var anyUser, data, emailUser, index, len, name, nullUser, obj, ref; + data = { + results: users, + }; + if (query.term.length === 0) { + if (firstUser) { + // Move current user to the front of the list + ref = data.results; + + for (index = 0, len = ref.length; index < len; index += 1) { + obj = ref[index]; + if (obj.username === firstUser) { + data.results.splice(index, 1); + data.results.unshift(obj); + break; + } } } - } - if (showNullUser) { - nullUser = { - name: 'Unassigned', - id: 0 - }; - data.results.unshift(nullUser); - } - if (showAnyUser) { - name = showAnyUser; - if (name === true) { - name = 'Any User'; + if (showNullUser) { + nullUser = { + name: 'Unassigned', + id: 0, + }; + data.results.unshift(nullUser); } - anyUser = { - name: name, - id: null + if (showAnyUser) { + name = showAnyUser; + if (name === true) { + name = 'Any User'; + } + anyUser = { + name: name, + id: null, + }; + data.results.unshift(anyUser); + } + } + if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { + var trimmed = query.term.trim(); + emailUser = { + name: 'Invite "' + trimmed + '" by email', + username: trimmed, + id: trimmed, + invite: true, }; - data.results.unshift(anyUser); + data.results.unshift(emailUser); } - } - if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { - var trimmed = query.term.trim(); - emailUser = { - name: "Invite \"" + trimmed + "\" by email", - username: trimmed, - id: trimmed, - invite: true - }; - data.results.unshift(emailUser); - } - return query.callback(data); - }); - }, - initSelection: function() { - var args; - args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; - return _this.initSelection.apply(_this, args); - }, - formatResult: function() { - var args; - args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; - return _this.formatResult.apply(_this, args); - }, - formatSelection: function() { - var args; - args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; - return _this.formatSelection.apply(_this, args); - }, - dropdownCssClass: "ajax-users-dropdown", - // we do not want to escape markup since we are displaying html in results - escapeMarkup: function(m) { - return m; - } - }); - }; - })(this)); + return query.callback(data); + }); + }, + initSelection: function() { + var args; + args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return _this.initSelection.apply(_this, args); + }, + formatResult: function() { + var args; + args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return _this.formatResult.apply(_this, args); + }, + formatSelection: function() { + var args; + args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return _this.formatSelection.apply(_this, args); + }, + dropdownCssClass: 'ajax-users-dropdown', + // we do not want to escape markup since we are displaying html in results + escapeMarkup: function(m) { + return m; + }, + }); + }; + })(this), + ); } UsersSelect.prototype.initSelection = function(element, callback) { var id, nullUser; id = $(element).val(); - if (id === "0") { + if (id === '0') { nullUser = { - name: 'Unassigned' + name: 'Unassigned', }; return callback(nullUser); - } else if (id !== "") { + } else if (id !== '') { return this.user(id, callback); } }; @@ -647,7 +696,17 @@ UsersSelect.prototype.formatResult = function(user) { } else { avatar = gon.default_avatar_url; } - return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + _.escape(user.name) + "</div> <div class='user-username dropdown-menu-user-username'>" + (!user.invite ? "@" + _.escape(user.username) : "") + "</div> </div>"; + return ( + "<div class='user-result " + + (!user.username ? 'no-username' : void 0) + + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + + avatar + + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + + _.escape(user.name) + + "</div> <div class='user-username dropdown-menu-user-username'>" + + (!user.invite ? '@' + _.escape(user.username) : '') + + '</div> </div>' + ); }; UsersSelect.prototype.formatSelection = function(user) { @@ -662,10 +721,9 @@ UsersSelect.prototype.user = function(user_id, callback) { var url; url = this.buildUrl(this.userPath); url = url.replace(':id', user_id); - return axios.get(url) - .then(({ data }) => { - callback(data); - }); + return axios.get(url).then(({ data }) => { + callback(data); + }); }; // Return users list. Filtered by query @@ -682,12 +740,11 @@ UsersSelect.prototype.users = function(query, options, callback) { todo_state_filter: options.todoStateFilter || null, current_user: options.showCurrentUser || null, author_id: options.authorId || null, - skip_users: options.skipUsers || null + skip_users: options.skipUsers || null, }; - return axios.get(url, { params }) - .then(({ data }) => { - callback(data); - }); + return axios.get(url, { params }).then(({ data }) => { + callback(data); + }); }; UsersSelect.prototype.buildUrl = function(url) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 70518ad73e8..9161f703697 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -22,9 +22,7 @@ export default { directives: { tooltip, }, - mixins: [ - timeagoMixin, - ], + mixins: [timeagoMixin], props: { deployment: { type: Object, @@ -50,7 +48,7 @@ export default { return !!(this.deployment.url && this.deployment.name); }, hasMetrics() { - return !!(this.deployment.metrics_url); + return !!this.deployment.metrics_url; }, }, methods: { @@ -63,7 +61,7 @@ export default { MRWidgetService.stopEnvironment(this.deployment.stop_url) .then(res => res.data) - .then((data) => { + .then(data => { if (data.redirect_url) { visitUrl(data.redirect_url); } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue index 5e76f6b1cac..41dbc5c9cbb 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue @@ -60,11 +60,29 @@ export default { let memoryUsageMsg = ''; if (memoryTo > memoryFrom) { - memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB'), messageProps, false); + memoryUsageMsg = sprintf( + s__( + 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB', + ), + messageProps, + false, + ); } else if (memoryTo < memoryFrom) { - memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB'), messageProps, false); + memoryUsageMsg = sprintf( + s__( + 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB', + ), + messageProps, + false, + ); } else { - memoryUsageMsg = sprintf(s__('mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB'), messageProps, false); + memoryUsageMsg = sprintf( + s__( + 'mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB', + ), + messageProps, + false, + ); } return memoryUsageMsg; @@ -77,7 +95,7 @@ export default { methods: { getMegabytes(bytesString) { const valueInBytes = Number(bytesString).toFixed(2); - return (bytesToMiB(valueInBytes)).toFixed(2); + return bytesToMiB(valueInBytes).toFixed(2); }, computeGraphData(metrics, deploymentTime) { this.loadingMetrics = false; @@ -103,7 +121,7 @@ export default { loadMetrics() { backOff((next, stop) => { MRWidgetService.fetchMetrics(this.metricsUrl) - .then((res) => { + .then(res => { if (res.status === statusCodes.NO_CONTENT) { this.backOffRequestCounter += 1; /* eslint-disable no-unused-expressions */ @@ -114,14 +132,14 @@ export default { }) .catch(stop); }) - .then((res) => { + .then(res => { if (res.status === statusCodes.NO_CONTENT) { return res; } return res.data; }) - .then((data) => { + .then(data => { this.computeGraphData(data.metrics, data.deployment_time); return data; }) diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue index 22c2f74f900..2ad9e8be655 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue @@ -1,36 +1,36 @@ <script> - import tooltip from '../../vue_shared/directives/tooltip'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - name: 'MrWidgetAuthor', - directives: { - tooltip, +export default { + name: 'MrWidgetAuthor', + directives: { + tooltip, + }, + props: { + author: { + type: Object, + required: true, }, - props: { - author: { - type: Object, - required: true, - }, - showAuthorName: { - type: Boolean, - required: false, - default: true, - }, - showAuthorTooltip: { - type: Boolean, - required: false, - default: false, - }, + showAuthorName: { + type: Boolean, + required: false, + default: true, }, - computed: { - authorUrl() { - return this.author.webUrl || this.author.web_url; - }, - avatarUrl() { - return this.author.avatarUrl || this.author.avatar_url; - }, + showAuthorTooltip: { + type: Boolean, + required: false, + default: false, }, - }; + }, + computed: { + authorUrl() { + return this.author.webUrl || this.author.web_url; + }, + avatarUrl() { + return this.author.avatarUrl || this.author.avatar_url; + }, + }, +}; </script> <template> <a diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue index ba16cb9e2c8..1d902131f49 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue @@ -1,34 +1,34 @@ <script> - import tooltip from '~/vue_shared/directives/tooltip'; - import MrWidgetAuthor from './mr_widget_author.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; +import MrWidgetAuthor from './mr_widget_author.vue'; - export default { - name: 'MrWidgetAuthorTime', - components: { - MrWidgetAuthor, +export default { + name: 'MrWidgetAuthorTime', + components: { + MrWidgetAuthor, + }, + directives: { + tooltip, + }, + props: { + actionText: { + type: String, + required: true, }, - directives: { - tooltip, + author: { + type: Object, + required: true, }, - props: { - actionText: { - type: String, - required: true, - }, - author: { - type: Object, - required: true, - }, - dateTitle: { - type: String, - required: true, - }, - dateReadable: { - type: String, - required: true, - }, + dateTitle: { + type: String, + required: true, }, - }; + dateReadable: { + type: String, + required: true, + }, + }, +}; </script> <template> <h4 class="js-mr-widget-author"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue index 62b61e1f41f..37c6af13c03 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue @@ -1,24 +1,26 @@ <script> - import { sprintf, s__ } from '~/locale'; +import { sprintf, s__ } from '~/locale'; - export default { - name: 'MRWidgetMergeHelp', - props: { - missingBranch: { - type: String, - required: false, - default: '', - }, +export default { + name: 'MRWidgetMergeHelp', + props: { + missingBranch: { + type: String, + required: false, + default: '', }, - computed: { - missingBranchInfo() { - return sprintf( - s__('mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the'), - { branch: this.missingBranch }, - ); - }, + }, + computed: { + missingBranchInfo() { + return sprintf( + s__( + 'mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the', + ), + { branch: this.missingBranch }, + ); }, - }; + }, +}; </script> <template> <section class="mr-widget-help"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue index 88d0fcd70f5..cc77b96a589 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue @@ -1,32 +1,32 @@ <script> - import { s__ } from '~/locale'; +import { s__ } from '~/locale'; - export default { - name: 'MRWidgetRelatedLinks', - props: { - relatedLinks: { - type: Object, - required: true, - default: () => ({}), - }, - state: { - type: String, - required: false, - default: '', - }, +export default { + name: 'MRWidgetRelatedLinks', + props: { + relatedLinks: { + type: Object, + required: true, + default: () => ({}), }, - computed: { - closesText() { - if (this.state === 'merged') { - return s__('mrWidget|Closed'); - } - if (this.state === 'closed') { - return s__('mrWidget|Did not close'); - } - return s__('mrWidget|Closes'); - }, + state: { + type: String, + required: false, + default: '', }, - }; + }, + computed: { + closesText() { + if (this.state === 'merged') { + return s__('mrWidget|Closed'); + } + if (this.state === 'closed') { + return s__('mrWidget|Did not close'); + } + return s__('mrWidget|Closes'); + }, + }, +}; </script> <template> <section class="mr-info-list mr-links"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue index 035ae791a1d..ba6a1687e51 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue @@ -1,33 +1,33 @@ <script> - import ciIcon from '../../vue_shared/components/ci_icon.vue'; +import ciIcon from '../../vue_shared/components/ci_icon.vue'; - export default { - components: { - ciIcon, +export default { + components: { + ciIcon, + }, + props: { + status: { + type: String, + required: true, }, - props: { - status: { - type: String, - required: true, - }, - showDisabledButton: { - type: Boolean, - required: false, - default: false, - }, + showDisabledButton: { + type: Boolean, + required: false, + default: false, }, - computed: { - isLoading() { - return this.status === 'loading'; - }, - statusObj() { - return { - group: this.status, - icon: `status_${this.status}`, - }; - }, + }, + computed: { + isLoading() { + return this.status === 'loading'; }, - }; + statusObj() { + return { + group: this.status, + icon: `status_${this.status}`, + }; + }, + }, +}; </script> <template> <div class="space-children d-flex append-right-10 widget-status-icon"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue index 56879c04d16..01f707163d4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue @@ -1,16 +1,16 @@ <script> - import tooltip from '../../vue_shared/directives/tooltip'; - import { __ } from '../../locale'; +import tooltip from '../../vue_shared/directives/tooltip'; +import { __ } from '../../locale'; - export default { - directives: { - tooltip, - }, - created() { - this.removesBranchText = __('<strong>Removes</strong> source branch'); - this.tooltipTitle = __('A user with write access to the source branch selected this option'); - }, - }; +export default { + directives: { + tooltip, + }, + created() { + this.removesBranchText = __('<strong>Removes</strong> source branch'); + this.tooltipTitle = __('A user with write access to the source branch selected this option'); + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue index cfbd44d41b2..2a76a878448 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue @@ -1,12 +1,12 @@ <script> - import statusIcon from '../mr_widget_status_icon.vue'; +import statusIcon from '../mr_widget_status_icon.vue'; - export default { - name: 'MRWidgetArchived', - components: { - statusIcon, - }, - }; +export default { + name: 'MRWidgetArchived', + components: { + statusIcon, + }, +}; </script> <template> <div class="mr-widget-body media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index 01294d5b40c..4f8b07484c0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -1,32 +1,32 @@ <script> - import eventHub from '../../event_hub'; - import statusIcon from '../mr_widget_status_icon.vue'; +import eventHub from '../../event_hub'; +import statusIcon from '../mr_widget_status_icon.vue'; - export default { - name: 'MRWidgetAutoMergeFailed', - components: { - statusIcon, +export default { + name: 'MRWidgetAutoMergeFailed', + components: { + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, }, - props: { - mr: { - type: Object, - required: true, - }, + }, + data() { + return { + isRefreshing: false, + }; + }, + methods: { + refreshWidget() { + this.isRefreshing = true; + eventHub.$emit('MRWidgetUpdateRequested', () => { + this.isRefreshing = false; + }); }, - data() { - return { - isRefreshing: false, - }; - }, - methods: { - refreshWidget() { - this.isRefreshing = true; - eventHub.$emit('MRWidgetUpdateRequested', () => { - this.isRefreshing = false; - }); - }, - }, - }; + }, +}; </script> <template> <div class="mr-widget-body media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue index ae6630dcd6f..fdf0a9fd994 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue @@ -1,12 +1,12 @@ <script> - import statusIcon from '../mr_widget_status_icon.vue'; +import statusIcon from '../mr_widget_status_icon.vue'; - export default { - name: 'MRWidgetChecking', - components: { - statusIcon, - }, - }; +export default { + name: 'MRWidgetChecking', + components: { + statusIcon, + }, +}; </script> <template> <div class="mr-widget-body media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue index 25a57e520ee..f06eab95c7e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue @@ -1,23 +1,23 @@ <script> - import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; - import statusIcon from '../mr_widget_status_icon.vue'; +import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; +import statusIcon from '../mr_widget_status_icon.vue'; - export default { - name: 'MRWidgetClosed', - components: { - MrWidgetAuthorTime, - statusIcon, - }, - props: { - /* TODO: This is providing all store and service down when it +export default { + name: 'MRWidgetClosed', + components: { + MrWidgetAuthorTime, + statusIcon, + }, + props: { + /* TODO: This is providing all store and service down when it only needs metrics and targetBranch */ - mr: { - type: Object, - required: true, - default: () => ({}), - }, + mr: { + type: Object, + required: true, + default: () => ({}), }, - }; + }, +}; </script> <template> <div class="mr-widget-body media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index dff9ec657b9..8c808296178 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -1,21 +1,21 @@ <script> - import statusIcon from '../mr_widget_status_icon.vue'; +import statusIcon from '../mr_widget_status_icon.vue'; - export default { - name: 'MRWidgetConflicts', - components: { - statusIcon, - }, - props: { - /* TODO: This is providing all store and service down when it +export default { + name: 'MRWidgetConflicts', + components: { + statusIcon, + }, + props: { + /* TODO: This is providing all store and service down when it only needs a few props */ - mr: { - type: Object, - required: true, - default: () => ({}), - }, + mr: { + type: Object, + required: true, + default: () => ({}), }, - }; + }, +}; </script> <template> <div class="mr-widget-body media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue index 97f4196b94d..484b5600d63 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue @@ -1,82 +1,82 @@ <script> - import Flash from '../../../flash'; - import statusIcon from '../mr_widget_status_icon.vue'; - import MrWidgetAuthor from '../../components/mr_widget_author.vue'; - import eventHub from '../../event_hub'; +import Flash from '../../../flash'; +import statusIcon from '../mr_widget_status_icon.vue'; +import MrWidgetAuthor from '../../components/mr_widget_author.vue'; +import eventHub from '../../event_hub'; - export default { - name: 'MRWidgetMergeWhenPipelineSucceeds', - components: { - MrWidgetAuthor, - statusIcon, +export default { + name: 'MRWidgetMergeWhenPipelineSucceeds', + components: { + MrWidgetAuthor, + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), }, - props: { - mr: { - type: Object, - required: true, - default: () => ({}), - }, - service: { - type: Object, - required: true, - default: () => ({}), - }, + service: { + type: Object, + required: true, + default: () => ({}), }, - data() { - return { - isCancellingAutoMerge: false, - isRemovingSourceBranch: false, - }; - }, - computed: { - canRemoveSourceBranch() { - const { - shouldRemoveSourceBranch, - canRemoveSourceBranch, - mergeUserId, - currentUserId, - } = this.mr; + }, + data() { + return { + isCancellingAutoMerge: false, + isRemovingSourceBranch: false, + }; + }, + computed: { + canRemoveSourceBranch() { + const { + shouldRemoveSourceBranch, + canRemoveSourceBranch, + mergeUserId, + currentUserId, + } = this.mr; - return !shouldRemoveSourceBranch && - canRemoveSourceBranch && - mergeUserId === currentUserId; - }, + return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId; }, - methods: { - cancelAutomaticMerge() { - this.isCancellingAutoMerge = true; - this.service.cancelAutomaticMerge() - .then(res => res.data) - .then((data) => { - eventHub.$emit('UpdateWidgetData', data); - }) - .catch(() => { - this.isCancellingAutoMerge = false; - Flash('Something went wrong. Please try again.'); - }); - }, - removeSourceBranch() { - const options = { - sha: this.mr.sha, - merge_when_pipeline_succeeds: true, - should_remove_source_branch: true, - }; + }, + methods: { + cancelAutomaticMerge() { + this.isCancellingAutoMerge = true; + this.service + .cancelAutomaticMerge() + .then(res => res.data) + .then(data => { + eventHub.$emit('UpdateWidgetData', data); + }) + .catch(() => { + this.isCancellingAutoMerge = false; + Flash('Something went wrong. Please try again.'); + }); + }, + removeSourceBranch() { + const options = { + sha: this.mr.sha, + merge_when_pipeline_succeeds: true, + should_remove_source_branch: true, + }; - this.isRemovingSourceBranch = true; - this.service.merge(options) - .then(res => res.data) - .then((data) => { - if (data.status === 'merge_when_pipeline_succeeds') { - eventHub.$emit('MRWidgetUpdateRequested'); - } - }) - .catch(() => { - this.isRemovingSourceBranch = false; - Flash('Something went wrong. Please try again.'); - }); - }, + this.isRemovingSourceBranch = true; + this.service + .merge(options) + .then(res => res.data) + .then(data => { + if (data.status === 'merge_when_pipeline_succeeds') { + eventHub.$emit('MRWidgetUpdateRequested'); + } + }) + .catch(() => { + this.isRemovingSourceBranch = false; + Flash('Something went wrong. Please try again.'); + }); }, - }; + }, +}; </script> <template> <div class="mr-widget-body media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 8184ef33022..656c3b5c47e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -1,106 +1,100 @@ <script> - import Flash from '~/flash'; - import tooltip from '~/vue_shared/directives/tooltip'; - import { s__, __ } from '~/locale'; - import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; - import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; - import statusIcon from '../mr_widget_status_icon.vue'; - import eventHub from '../../event_hub'; +import Flash from '~/flash'; +import tooltip from '~/vue_shared/directives/tooltip'; +import { s__, __ } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; +import statusIcon from '../mr_widget_status_icon.vue'; +import eventHub from '../../event_hub'; - export default { - name: 'MRWidgetMerged', - directives: { - tooltip, +export default { + name: 'MRWidgetMerged', + directives: { + tooltip, + }, + components: { + MrWidgetAuthorTime, + statusIcon, + ClipboardButton, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), }, - components: { - MrWidgetAuthorTime, - statusIcon, - ClipboardButton, + service: { + type: Object, + required: true, + default: () => ({}), }, - props: { - mr: { - type: Object, - required: true, - default: () => ({}), - }, - service: { - type: Object, - required: true, - default: () => ({}), - }, + }, + data() { + return { + isMakingRequest: false, + }; + }, + computed: { + shouldShowRemoveSourceBranch() { + const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr; + + return ( + !sourceBranchRemoved && + canRemoveSourceBranch && + !this.isMakingRequest && + !isRemovingSourceBranch + ); }, - data() { - return { - isMakingRequest: false, - }; + shouldShowSourceBranchRemoving() { + const { sourceBranchRemoved, isRemovingSourceBranch } = this.mr; + return !sourceBranchRemoved && (isRemovingSourceBranch || this.isMakingRequest); }, - computed: { - shouldShowRemoveSourceBranch() { - const { - sourceBranchRemoved, - isRemovingSourceBranch, - canRemoveSourceBranch, - } = this.mr; - - return !sourceBranchRemoved && - canRemoveSourceBranch && - !this.isMakingRequest && - !isRemovingSourceBranch; - }, - shouldShowSourceBranchRemoving() { - const { - sourceBranchRemoved, - isRemovingSourceBranch, - } = this.mr; - return !sourceBranchRemoved && - (isRemovingSourceBranch || this.isMakingRequest); - }, - shouldShowMergedButtons() { - const { - canRevertInCurrentMR, - canCherryPickInCurrentMR, - revertInForkPath, - cherryPickInForkPath, - } = this.mr; + shouldShowMergedButtons() { + const { + canRevertInCurrentMR, + canCherryPickInCurrentMR, + revertInForkPath, + cherryPickInForkPath, + } = this.mr; - return canRevertInCurrentMR || - canCherryPickInCurrentMR || - revertInForkPath || - cherryPickInForkPath; - }, - revertTitle() { - return s__('mrWidget|Revert this merge request in a new merge request'); - }, - cherryPickTitle() { - return s__('mrWidget|Cherry-pick this merge request in a new merge request'); - }, - revertLabel() { - return s__('mrWidget|Revert'); - }, - cherryPickLabel() { - return s__('mrWidget|Cherry-pick'); - }, + return ( + canRevertInCurrentMR || canCherryPickInCurrentMR || revertInForkPath || cherryPickInForkPath + ); + }, + revertTitle() { + return s__('mrWidget|Revert this merge request in a new merge request'); + }, + cherryPickTitle() { + return s__('mrWidget|Cherry-pick this merge request in a new merge request'); }, - methods: { - removeSourceBranch() { - this.isMakingRequest = true; + revertLabel() { + return s__('mrWidget|Revert'); + }, + cherryPickLabel() { + return s__('mrWidget|Cherry-pick'); + }, + }, + methods: { + removeSourceBranch() { + this.isMakingRequest = true; - this.service.removeSourceBranch() - .then(res => res.data) - .then((data) => { - if (data.message === 'Branch was removed') { - eventHub.$emit('MRWidgetUpdateRequested', () => { - this.isMakingRequest = false; - }); - } - }) - .catch(() => { - this.isMakingRequest = false; - Flash(__('Something went wrong. Please try again.')); - }); - }, + this.service + .removeSourceBranch() + .then(res => res.data) + .then(data => { + if (data.message === 'Branch was removed') { + eventHub.$emit('MRWidgetUpdateRequested', () => { + this.isMakingRequest = false; + }); + } + }) + .catch(() => { + this.isMakingRequest = false; + Flash(__('Something went wrong. Please try again.')); + }); }, - }; + }, +}; </script> <template> <div class="mr-widget-body media"> @@ -162,18 +156,20 @@ <span class="label-branch"> <a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a> </span> - with - <a - :href="mr.mergeCommitPath" - class="commit-sha js-mr-merged-commit-sha" - v-text="mr.shortMergeCommitSha" - > - </a> - <clipboard-button - :title="__('Copy commit SHA to clipboard')" - :text="mr.mergeCommitSha" - css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha" - /> + <template v-if="mr.mergeCommitSha"> + with + <a + :href="mr.mergeCommitPath" + class="commit-sha js-mr-merged-commit-sha" + v-text="mr.shortMergeCommitSha" + > + </a> + <clipboard-button + :title="__('Copy commit SHA to clipboard')" + :text="mr.mergeCommitSha" + css-class="btn-default btn-transparent btn-clipboard js-mr-merged-copy-sha" + /> + </template> </p> <p v-if="mr.sourceBranchRemoved"> {{ s__("mrWidget|The source branch has been removed") }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue index 953ddf40a51..139e64d1878 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue @@ -1,19 +1,19 @@ <script> - import statusIcon from '../mr_widget_status_icon.vue'; +import statusIcon from '../mr_widget_status_icon.vue'; - export default { - name: 'MRWidgetMerging', - components: { - statusIcon, +export default { + name: 'MRWidgetMerging', + components: { + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, + default: () => ({}), }, - props: { - mr: { - type: Object, - required: true, - default: () => ({}), - }, - }, - }; + }, +}; </script> <template> <div class="mr-widget-body mr-state-locked media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue index b0e96f74626..227e9b85f9d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue @@ -1,40 +1,48 @@ <script> - import { sprintf, s__ } from '~/locale'; - import tooltip from '~/vue_shared/directives/tooltip'; - import statusIcon from '../mr_widget_status_icon.vue'; - import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue'; +import { sprintf, s__ } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import statusIcon from '../mr_widget_status_icon.vue'; +import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue'; - export default { - name: 'MRWidgetMissingBranch', - directives: { - tooltip, +export default { + name: 'MRWidgetMissingBranch', + directives: { + tooltip, + }, + components: { + mrWidgetMergeHelp, + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, }, - components: { - mrWidgetMergeHelp, - statusIcon, + }, + computed: { + missingBranchName() { + return this.mr.sourceBranchRemoved ? 'source' : 'target'; }, - props: { - mr: { - type: Object, - required: true, - }, - }, - computed: { - missingBranchName() { - return this.mr.sourceBranchRemoved ? 'source' : 'target'; - }, - missingBranchNameMessage() { - return sprintf(s__('mrWidget| Please restore it or use a different %{missingBranchName} branch'), { + missingBranchNameMessage() { + return sprintf( + s__('mrWidget| Please restore it or use a different %{missingBranchName} branch'), + { missingBranchName: this.missingBranchName, - }); - }, - message() { - return sprintf(s__('mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line'), { + }, + ); + }, + message() { + return sprintf( + s__( + 'mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line', + ), + { missingBranchName: this.missingBranchName, - }); - }, + }, + ); }, - }; + }, +}; </script> <template> <div class="mr-widget-body media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue index 92eee2cf5dd..360559ae0f0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue @@ -1,12 +1,12 @@ <script> - import StatusIcon from '../mr_widget_status_icon.vue'; +import StatusIcon from '../mr_widget_status_icon.vue'; - export default { - name: 'MRWidgetNotAllowed', - components: { - StatusIcon, - }, - }; +export default { + name: 'MRWidgetNotAllowed', + components: { + StatusIcon, + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue index 37ee5215cea..a4eb5afb21c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue @@ -1,12 +1,12 @@ <script> - import StatusIcon from '../mr_widget_status_icon.vue'; +import StatusIcon from '../mr_widget_status_icon.vue'; - export default { - name: 'MRWidgetPipelineBlocked', - components: { - StatusIcon, - }, - }; +export default { + name: 'MRWidgetPipelineBlocked', + components: { + StatusIcon, + }, +}; </script> <template> <div class="mr-widget-body media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index f31c7a3edb8..041fa13a8f5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -1,85 +1,87 @@ <script> - import simplePoll from '../../../lib/utils/simple_poll'; - import eventHub from '../../event_hub'; - import statusIcon from '../mr_widget_status_icon.vue'; - import Flash from '../../../flash'; +import simplePoll from '../../../lib/utils/simple_poll'; +import eventHub from '../../event_hub'; +import statusIcon from '../mr_widget_status_icon.vue'; +import Flash from '../../../flash'; - export default { - name: 'MRWidgetRebase', - components: { - statusIcon, +export default { + name: 'MRWidgetRebase', + components: { + statusIcon, + }, + props: { + mr: { + type: Object, + required: true, }, - props: { - mr: { - type: Object, - required: true, - }, - service: { - type: Object, - required: true, - }, + service: { + type: Object, + required: true, }, - data() { - return { - isMakingRequest: false, - rebasingError: null, - }; + }, + data() { + return { + isMakingRequest: false, + rebasingError: null, + }; + }, + computed: { + status() { + if (this.mr.rebaseInProgress || this.isMakingRequest) { + return 'loading'; + } + if (!this.mr.canPushToSourceBranch && !this.mr.rebaseInProgress) { + return 'warning'; + } + return 'success'; }, - computed: { - status() { - if (this.mr.rebaseInProgress || this.isMakingRequest) { - return 'loading'; - } - if (!this.mr.canPushToSourceBranch && !this.mr.rebaseInProgress) { - return 'warning'; - } - return 'success'; - }, - showDisabledButton() { - return ['failed', 'loading'].includes(this.status); - }, + showDisabledButton() { + return ['failed', 'loading'].includes(this.status); }, - methods: { - rebase() { - this.isMakingRequest = true; - this.rebasingError = null; + }, + methods: { + rebase() { + this.isMakingRequest = true; + this.rebasingError = null; - this.service.rebase() - .then(() => { - simplePoll(this.checkRebaseStatus); - }) - .catch((error) => { - this.rebasingError = error.merge_error; + this.service + .rebase() + .then(() => { + simplePoll(this.checkRebaseStatus); + }) + .catch(error => { + this.rebasingError = error.merge_error; + this.isMakingRequest = false; + Flash('Something went wrong. Please try again.'); + }); + }, + checkRebaseStatus(continuePolling, stopPolling) { + this.service + .poll() + .then(res => res.data) + .then(res => { + if (res.rebase_in_progress) { + continuePolling(); + } else { this.isMakingRequest = false; - Flash('Something went wrong. Please try again.'); - }); - }, - checkRebaseStatus(continuePolling, stopPolling) { - this.service.poll() - .then(res => res.data) - .then((res) => { - if (res.rebase_in_progress) { - continuePolling(); - } else { - this.isMakingRequest = false; - - if (res.merge_error && res.merge_error.length) { - this.rebasingError = res.merge_error; - Flash('Something went wrong. Please try again.'); - } - eventHub.$emit('MRWidgetUpdateRequested'); - stopPolling(); + if (res.merge_error && res.merge_error.length) { + this.rebasingError = res.merge_error; + Flash('Something went wrong. Please try again.'); } - }) - .catch(() => { - this.isMakingRequest = false; - Flash('Something went wrong. Please try again.'); + + eventHub.$emit('MRWidgetUpdateRequested'); stopPolling(); - }); - }, + } + }) + .catch(() => { + this.isMakingRequest = false; + Flash('Something went wrong. Please try again.'); + stopPolling(); + }); }, - }; + }, +}; </script> <template> <div class="mr-widget-body media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 89c9a41f316..9129fcbb918 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -24,9 +24,10 @@ export default { methods: { removeWIP() { this.isMakingRequest = true; - this.service.removeWIP() + this.service + .removeWIP() .then(res => res.data) - .then((data) => { + .then(data => { eventHub.$emit('UpdateWidgetData', data); new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line $('.merge-request .detail-page-description .title').text(this.mr.title); diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 0e445a29de4..a599cc002d6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -1,5 +1,4 @@ <script> - import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; import createFlash from '../flash'; @@ -100,8 +99,11 @@ export default { return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState; }, shouldRenderSourceBranchRemovalStatus() { - return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch && - (!this.mr.isNothingToMergeState && !this.mr.isMergedState); + return ( + !this.mr.canRemoveSourceBranch && + this.mr.shouldRemoveSourceBranch && + (!this.mr.isNothingToMergeState && !this.mr.isMergedState) + ); }, }, created() { @@ -133,9 +135,10 @@ export default { return new MRWidgetService(endpoints); }, checkStatus(cb) { - return this.service.checkStatus() + return this.service + .checkStatus() .then(res => res.data) - .then((data) => { + .then(data => { this.handleNotification(data); this.mr.setData(data); this.setFaviconHelper(); @@ -172,20 +175,24 @@ export default { return Promise.resolve(); }, fetchDeployments() { - return this.service.fetchDeployments() + return this.service + .fetchDeployments() .then(res => res.data) - .then((data) => { + .then(data => { if (data.length) { this.mr.deployments = data; } }) .catch(() => { - createFlash('Something went wrong while fetching the environments for this merge request. Please try again.'); + createFlash( + 'Something went wrong while fetching the environments for this merge request. Please try again.', + ); }); }, fetchActionsContent() { - this.service.fetchMergeActionsContent() - .then((res) => { + this.service + .fetchMergeActionsContent() + .then(res => { if (res.data) { const el = document.createElement('div'); el.innerHTML = res.data; @@ -212,22 +219,22 @@ export default { this.pollingInterval.stopTimer(); }, bindEventHubListeners() { - eventHub.$on('MRWidgetUpdateRequested', (cb) => { + eventHub.$on('MRWidgetUpdateRequested', cb => { this.checkStatus(cb); }); // `params` should be an Array contains a Boolean, like `[true]` // Passing parameter as Boolean didn't work. - eventHub.$on('SetBranchRemoveFlag', (params) => { + eventHub.$on('SetBranchRemoveFlag', params => { [this.mr.isRemovingSourceBranch] = params; }); - eventHub.$on('FailedToMerge', (mergeError) => { + eventHub.$on('FailedToMerge', mergeError => { this.mr.state = 'failedToMerge'; this.mr.mergeError = mergeError; }); - eventHub.$on('UpdateWidgetData', (data) => { + eventHub.$on('UpdateWidgetData', data => { this.mr.setData(data); }); @@ -271,8 +278,10 @@ export default { <div class="mr-section-container"> <grouped-test-reports-app v-if="mr.testResultsPath" + class="js-reports-container" :endpoint="mr.testResultsPath" /> + <div class="mr-widget-section"> <component :is="componentName" diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue index 33af7a7f1df..690dd794ba4 100644 --- a/app/assets/javascripts/vue_shared/components/bar_chart.vue +++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue @@ -118,7 +118,9 @@ export default { this.rectYAxisLabelDims.height != null ? this.rectYAxisLabelDims.height / 2 : 0; const yCoord = this.vbHeight / 2 + rectWidth - 5; - return `translate(${this.minX + this.yAxisTextTransformPadding}, ${yCoord}) rotate(-${this.yAxisTextRotation})`; + return `translate(${this.minX + this.yAxisTextTransformPadding}, ${yCoord}) rotate(-${ + this.yAxisTextRotation + })`; }, }, mounted() { @@ -207,8 +209,7 @@ export default { renderedYAxis.selectAll('.tick').each(function createTickLines(d, i) { if (i > 0) { - d3 - .select(this) + d3.select(this) .select('line') .attr('x2', width) .attr('class', 'axis-tick'); @@ -217,8 +218,7 @@ export default { // Add the panning capabilities if (this.isPanAvailable) { - d3 - .select(this.$refs.baseSvg) + d3.select(this.$refs.baseSvg) .call(this.zoom) .on('wheel.zoom', null); // This disables the pan of the graph with the scroll of the mouse wheel } diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index a2518e2a611..c60052fec50 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -43,7 +43,7 @@ export default { computed: { cssClass() { const className = this.status.group; - return className ? `ci-status ci-${className}` : 'ci-status'; + return className ? `ci-status ci-${className} qa-status-badge` : 'ci-status qa-status-badge'; }, }, }; diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue index 9c1e5c68649..4d63296e332 100644 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue @@ -1,85 +1,85 @@ <script> - /* eslint-disable vue/require-default-prop */ - export default { - name: 'DeprecatedModal', // use GlModal instead +/* eslint-disable vue/require-default-prop */ +export default { + name: 'DeprecatedModal', // use GlModal instead - props: { - id: { - type: String, - required: false, - }, - title: { - type: String, - required: false, - }, - text: { - type: String, - required: false, - }, - hideFooter: { - type: Boolean, - required: false, - default: false, - }, - kind: { - type: String, - required: false, - default: 'primary', - }, - modalDialogClass: { - type: String, - required: false, - default: '', - }, - closeKind: { - type: String, - required: false, - default: 'default', - }, - closeButtonLabel: { - type: String, - required: false, - default: 'Cancel', - }, - primaryButtonLabel: { - type: String, - required: false, - default: '', - }, - secondaryButtonLabel: { - type: String, - required: false, - default: '', - }, - submitDisabled: { - type: Boolean, - required: false, - default: false, - }, + props: { + id: { + type: String, + required: false, }, + title: { + type: String, + required: false, + }, + text: { + type: String, + required: false, + }, + hideFooter: { + type: Boolean, + required: false, + default: false, + }, + kind: { + type: String, + required: false, + default: 'primary', + }, + modalDialogClass: { + type: String, + required: false, + default: '', + }, + closeKind: { + type: String, + required: false, + default: 'default', + }, + closeButtonLabel: { + type: String, + required: false, + default: 'Cancel', + }, + primaryButtonLabel: { + type: String, + required: false, + default: '', + }, + secondaryButtonLabel: { + type: String, + required: false, + default: '', + }, + submitDisabled: { + type: Boolean, + required: false, + default: false, + }, + }, - computed: { - btnKindClass() { - return { - [`btn-${this.kind}`]: true, - }; - }, - btnCancelKindClass() { - return { - [`btn-${this.closeKind}`]: true, - }; - }, + computed: { + btnKindClass() { + return { + [`btn-${this.kind}`]: true, + }; + }, + btnCancelKindClass() { + return { + [`btn-${this.closeKind}`]: true, + }; }, + }, - methods: { - emitCancel(event) { - this.$emit('cancel', event); - }, - emitSubmit(event) { - this.$emit('submit', event); - }, + methods: { + emitCancel(event) { + this.$emit('cancel', event); + }, + emitSubmit(event) { + this.$emit('submit', event); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index b371b6adf7e..aee88cae48d 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -69,6 +69,9 @@ export default { onClickAction(action) { this.$emit('actionClicked', action); }, + onClickSidebarButton() { + this.$emit('clickedSidebarButton'); + }, }, }; </script> @@ -161,21 +164,21 @@ export default { </i> </button> </template> - <button - v-if="hasSidebarButton" - id="toggleSidebar" - type="button" - class="btn btn-default d-block d-sm-none d-md-none + </section> + <button + v-if="hasSidebarButton" + id="toggleSidebar" + type="button" + class="btn btn-default d-block d-sm-none sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" - aria-label="Toggle Sidebar" + @click="onClickSidebarButton" + > + <i + class="fa fa-angle-double-left" + aria-hidden="true" + aria-labelledby="toggleSidebar" > - <i - class="fa fa-angle-double-left" - aria-hidden="true" - aria-labelledby="toggleSidebar" - > - </i> - </button> - </section> + </i> + </button> </header> </template> diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 5e0e7315e99..26f9d5ddc91 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -1,5 +1,4 @@ <script> - // only allow classes in images.scss e.g. s12 const validSizes = [8, 10, 12, 16, 18, 24, 32, 48, 72]; let iconValidator = () => true; @@ -105,6 +104,7 @@ export default { :x="x" :y="y" :tabindex="tabIndex" + aria-hidden="true" > <use v-bind="{ 'xlink:href':spriteHref }"/> </svg> diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index ca8ce554588..dc88749c18f 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -1,34 +1,34 @@ <script> - import icon from '../../../vue_shared/components/icon.vue'; +import icon from '../../../vue_shared/components/icon.vue'; - export default { - components: { - icon, +export default { + components: { + icon, + }, + props: { + isLocked: { + type: Boolean, + default: false, + required: false, }, - props: { - isLocked: { - type: Boolean, - default: false, - required: false, - }, - isConfidential: { - type: Boolean, - default: false, - required: false, - }, + isConfidential: { + type: Boolean, + default: false, + required: false, }, - computed: { - warningIcon() { - if (this.isConfidential) return 'eye-slash'; - if (this.isLocked) return 'lock'; + }, + computed: { + warningIcon() { + if (this.isConfidential) return 'eye-slash'; + if (this.isLocked) return 'lock'; - return ''; - }, - isLockedAndConfidential() { - return this.isConfidential && this.isLocked; - }, + return ''; }, - }; + isLockedAndConfidential() { + return this.isConfidential && this.isLocked; + }, + }, +}; </script> <template> <div class="issuable-note-warning"> @@ -37,7 +37,6 @@ :name="warningIcon" :size="16" class="icon inline" - aria-hidden="true" /> <span v-if="isLockedAndConfidential"> diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 4cbd3e6429d..f9b7fd5b1f9 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -1,6 +1,6 @@ <script> - /* eslint-disable vue/require-default-prop */ - /* This is a re-usable vue component for rendering a button +/* eslint-disable vue/require-default-prop */ +/* This is a re-usable vue component for rendering a button that will probably be sending off ajax requests and need to show the loading status by setting the `loading` option. This can also be used for initial page load when you don't @@ -17,34 +17,34 @@ */ - export default { - props: { - loading: { - type: Boolean, - required: false, - default: false, - }, - disabled: { - type: Boolean, - required: false, - default: false, - }, - label: { - type: String, - required: false, - }, - containerClass: { - type: [String, Array, Object], - required: false, - default: 'btn btn-align-content', - }, +export default { + props: { + loading: { + type: Boolean, + required: false, + default: false, }, - methods: { - onClick(e) { - this.$emit('click', e); - }, + disabled: { + type: Boolean, + required: false, + default: false, }, - }; + label: { + type: String, + required: false, + }, + containerClass: { + type: [String, Array, Object], + required: false, + default: 'btn btn-align-content', + }, + }, + methods: { + onClick(e) { + this.$emit('click', e); + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 10e8ddad9cd..4687de62614 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -1,141 +1,141 @@ <script> - import $ from 'jquery'; - import { s__ } from '~/locale'; - import Flash from '../../../flash'; - import GLForm from '../../../gl_form'; - import markdownHeader from './header.vue'; - import markdownToolbar from './toolbar.vue'; - import icon from '../icon.vue'; +import $ from 'jquery'; +import { s__ } from '~/locale'; +import Flash from '../../../flash'; +import GLForm from '../../../gl_form'; +import markdownHeader from './header.vue'; +import markdownToolbar from './toolbar.vue'; +import icon from '../icon.vue'; - export default { - components: { - markdownHeader, - markdownToolbar, - icon, +export default { + components: { + markdownHeader, + markdownToolbar, + icon, + }, + props: { + markdownPreviewPath: { + type: String, + required: false, + default: '', }, - props: { - markdownPreviewPath: { - type: String, - required: false, - default: '', - }, - markdownDocsPath: { - type: String, - required: true, - }, - markdownVersion: { - type: Number, - required: false, - default: 0, - }, - addSpacingClasses: { - type: Boolean, - required: false, - default: true, - }, - quickActionsDocsPath: { - type: String, - required: false, - default: '', - }, - canAttachFile: { - type: Boolean, - required: false, - default: true, - }, - enableAutocomplete: { - type: Boolean, - required: false, - default: true, - }, + markdownDocsPath: { + type: String, + required: true, }, - data() { - return { - markdownPreview: '', - referencedCommands: '', - referencedUsers: '', - markdownPreviewLoading: false, - previewMarkdown: false, - }; + markdownVersion: { + type: Number, + required: false, + default: 0, }, - computed: { - shouldShowReferencedUsers() { - const referencedUsersThreshold = 10; - return this.referencedUsers.length >= referencedUsersThreshold; - }, + addSpacingClasses: { + type: Boolean, + required: false, + default: true, }, - mounted() { - /* - GLForm class handles all the toolbar buttons - */ - return new GLForm($(this.$refs['gl-form']), { - emojis: this.enableAutocomplete, - members: this.enableAutocomplete, - issues: this.enableAutocomplete, - mergeRequests: this.enableAutocomplete, - epics: this.enableAutocomplete, - milestones: this.enableAutocomplete, - labels: this.enableAutocomplete, - snippets: this.enableAutocomplete, - }); + quickActionsDocsPath: { + type: String, + required: false, + default: '', }, - beforeDestroy() { - const glForm = $(this.$refs['gl-form']).data('glForm'); - if (glForm) { - glForm.destroy(); - } + canAttachFile: { + type: Boolean, + required: false, + default: true, + }, + enableAutocomplete: { + type: Boolean, + required: false, + default: true, }, - methods: { - showPreviewTab() { - if (this.previewMarkdown) return; + }, + data() { + return { + markdownPreview: '', + referencedCommands: '', + referencedUsers: '', + markdownPreviewLoading: false, + previewMarkdown: false, + }; + }, + computed: { + shouldShowReferencedUsers() { + const referencedUsersThreshold = 10; + return this.referencedUsers.length >= referencedUsersThreshold; + }, + }, + mounted() { + /* + GLForm class handles all the toolbar buttons + */ + return new GLForm($(this.$refs['gl-form']), { + emojis: this.enableAutocomplete, + members: this.enableAutocomplete, + issues: this.enableAutocomplete, + mergeRequests: this.enableAutocomplete, + epics: this.enableAutocomplete, + milestones: this.enableAutocomplete, + labels: this.enableAutocomplete, + snippets: this.enableAutocomplete, + }); + }, + beforeDestroy() { + const glForm = $(this.$refs['gl-form']).data('glForm'); + if (glForm) { + glForm.destroy(); + } + }, + methods: { + showPreviewTab() { + if (this.previewMarkdown) return; - this.previewMarkdown = true; + this.previewMarkdown = true; - /* + /* Can't use `$refs` as the component is technically in the parent component so we access the VNode & then get the element */ - const text = this.$slots.textarea[0].elm.value; + const text = this.$slots.textarea[0].elm.value; - if (text) { - this.markdownPreviewLoading = true; - this.$http - .post(this.versionedPreviewPath(), { text }) - .then(resp => resp.json()) - .then(data => this.renderMarkdown(data)) - .catch(() => new Flash(s__('Error loading markdown preview'))); - } else { - this.renderMarkdown(); - } - }, + if (text) { + this.markdownPreviewLoading = true; + this.$http + .post(this.versionedPreviewPath(), { text }) + .then(resp => resp.json()) + .then(data => this.renderMarkdown(data)) + .catch(() => new Flash(s__('Error loading markdown preview'))); + } else { + this.renderMarkdown(); + } + }, - showWriteTab() { - this.markdownPreview = ''; - this.previewMarkdown = false; - }, + showWriteTab() { + this.markdownPreview = ''; + this.previewMarkdown = false; + }, - renderMarkdown(data = {}) { - this.markdownPreviewLoading = false; - this.markdownPreview = data.body || 'Nothing to preview.'; + renderMarkdown(data = {}) { + this.markdownPreviewLoading = false; + this.markdownPreview = data.body || 'Nothing to preview.'; - if (data.references) { - this.referencedCommands = data.references.commands; - this.referencedUsers = data.references.users; - } + if (data.references) { + this.referencedCommands = data.references.commands; + this.referencedUsers = data.references.users; + } - this.$nextTick(() => { - $(this.$refs['markdown-preview']).renderGFM(); - }); - }, + this.$nextTick(() => { + $(this.$refs['markdown-preview']).renderGFM(); + }); + }, - versionedPreviewPath() { - const { markdownPreviewPath, markdownVersion } = this; - return `${markdownPreviewPath}${ - markdownPreviewPath.indexOf('?') === -1 ? '?' : '&' - }markdown_version=${markdownVersion}`; - }, + versionedPreviewPath() { + const { markdownPreviewPath, markdownVersion } = this; + return `${markdownPreviewPath}${ + markdownPreviewPath.indexOf('?') === -1 ? '?' : '&' + }markdown_version=${markdownVersion}`; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index ccd53158820..704adf7864f 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,63 +1,64 @@ <script> - import $ from 'jquery'; - import tooltip from '../../directives/tooltip'; - import toolbarButton from './toolbar_button.vue'; - import icon from '../icon.vue'; +import $ from 'jquery'; +import tooltip from '../../directives/tooltip'; +import toolbarButton from './toolbar_button.vue'; +import icon from '../icon.vue'; - export default { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + toolbarButton, + icon, + }, + props: { + previewMarkdown: { + type: Boolean, + required: true, }, - components: { - toolbarButton, - icon, + }, + computed: { + mdTable() { + return [ + '| header | header |', + '| ------ | ------ |', + '| cell | cell |', + '| cell | cell |', + ].join('\n'); }, - props: { - previewMarkdown: { - type: Boolean, - required: true, - }, + }, + mounted() { + $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); + $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); + }, + beforeDestroy() { + $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); + $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab); + }, + methods: { + isValid(form) { + return ( + !form || + (form.find('.js-vue-markdown-field').length && $(this.$el).closest('form')[0] === form[0]) + ); }, - computed: { - mdTable() { - return [ - '| header | header |', - '| ------ | ------ |', - '| cell | cell |', - '| cell | cell |', - ].join('\n'); - }, - }, - mounted() { - $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); - $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); - }, - beforeDestroy() { - $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); - $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab); - }, - methods: { - isValid(form) { - return !form || - form.find('.js-vue-markdown-field').length && - $(this.$el).closest('form')[0] === form[0]; - }, - previewMarkdownTab(event, form) { - if (event.target.blur) event.target.blur(); - if (!this.isValid(form)) return; + previewMarkdownTab(event, form) { + if (event.target.blur) event.target.blur(); + if (!this.isValid(form)) return; - this.$emit('preview-markdown'); - }, + this.$emit('preview-markdown'); + }, - writeMarkdownTab(event, form) { - if (event.target.blur) event.target.blur(); - if (!this.isValid(form)) return; + writeMarkdownTab(event, form) { + if (event.target.blur) event.target.blur(); + if (!this.isValid(form)) return; - this.$emit('write-markdown'); - }, + this.$emit('write-markdown'); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index c45dafa9807..feb7b8f227e 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,32 +1,32 @@ <script> - import { Link } from '@gitlab-org/gitlab-ui'; +import { Link } from '@gitlab-org/gitlab-ui'; - export default { - components: { - 'gl-link': Link, +export default { + components: { + 'gl-link': Link, + }, + props: { + markdownDocsPath: { + type: String, + required: true, }, - props: { - markdownDocsPath: { - type: String, - required: true, - }, - quickActionsDocsPath: { - type: String, - required: false, - default: '', - }, - canAttachFile: { - type: Boolean, - required: false, - default: true, - }, + quickActionsDocsPath: { + type: String, + required: false, + default: '', }, - computed: { - hasQuickActionsDocsPath() { - return this.quickActionsDocsPath !== ''; - }, + canAttachFile: { + type: Boolean, + required: false, + default: true, }, - }; + }, + computed: { + hasQuickActionsDocsPath() { + return this.quickActionsDocsPath !== ''; + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index bda33636369..3e89e1c1e75 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -1,44 +1,44 @@ <script> - import tooltip from '../../directives/tooltip'; - import icon from '../icon.vue'; +import tooltip from '../../directives/tooltip'; +import icon from '../icon.vue'; - export default { - components: { - icon, +export default { + components: { + icon, + }, + directives: { + tooltip, + }, + props: { + buttonTitle: { + type: String, + required: true, }, - directives: { - tooltip, + icon: { + type: String, + required: true, }, - props: { - buttonTitle: { - type: String, - required: true, - }, - icon: { - type: String, - required: true, - }, - tag: { - type: String, - required: true, - }, - tagBlock: { - type: String, - required: false, - default: '', - }, - tagSelect: { - type: String, - required: false, - default: '', - }, - prepend: { - type: Boolean, - required: false, - default: false, - }, + tag: { + type: String, + required: true, }, - }; + tagBlock: { + type: String, + required: false, + default: '', + }, + tagSelect: { + type: String, + required: false, + default: '', + }, + prepend: { + type: Boolean, + required: false, + default: false, + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.vue b/app/assets/javascripts/vue_shared/components/memory_graph.vue index 552a92541be..964dedb38c4 100644 --- a/app/assets/javascripts/vue_shared/components/memory_graph.vue +++ b/app/assets/javascripts/vue_shared/components/memory_graph.vue @@ -41,7 +41,8 @@ export default { // Find metric timestamp which is closest to deploymentTime timestampDiff = Math.abs(metricTimestamps[0] - median); metricTimestamps.forEach((timestamp, index) => { - if (index === 0) { // Skip first element + if (index === 0) { + // Skip first element return; } diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index 38115f268bb..dcad79e521d 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -1,41 +1,39 @@ <script> - /** - * Common component to render a placeholder note and user information. - * - * This component needs to be used with a vuex store. - * That vuex store needs to have a `getUserData` getter that contains - * { - * path: String, - * avatar_url: String, - * name: String, - * username: String, - * } - * - * @example - * <placeholder-note - * :note="{body: 'This is a note'}" - * /> - */ - import { mapGetters } from 'vuex'; - import userAvatarLink from '../user_avatar/user_avatar_link.vue'; +/** + * Common component to render a placeholder note and user information. + * + * This component needs to be used with a vuex store. + * That vuex store needs to have a `getUserData` getter that contains + * { + * path: String, + * avatar_url: String, + * name: String, + * username: String, + * } + * + * @example + * <placeholder-note + * :note="{body: 'This is a note'}" + * /> + */ +import { mapGetters } from 'vuex'; +import userAvatarLink from '../user_avatar/user_avatar_link.vue'; - export default { - name: 'PlaceholderNote', - components: { - userAvatarLink, +export default { + name: 'PlaceholderNote', + components: { + userAvatarLink, + }, + props: { + note: { + type: Object, + required: true, }, - props: { - note: { - type: Object, - required: true, - }, - }, - computed: { - ...mapGetters([ - 'getUserData', - ]), - }, - }; + }, + computed: { + ...mapGetters(['getUserData']), + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue index 95e2b38e292..674f923478d 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_system_note.vue @@ -1,21 +1,21 @@ <script> - /** - * Common component to render a placeholder system note. - * - * @example - * <placeholder-system-note - * :note="{ body: 'Commands are being applied'}" - * /> - */ - export default { - name: 'PlaceholderSystemNote', - props: { - note: { - type: Object, - required: true, - }, +/** + * Common component to render a placeholder system note. + * + * @example + * <placeholder-system-note + * :note="{ body: 'Commands are being applied'}" + * /> + */ +export default { + name: 'PlaceholderSystemNote', + props: { + note: { + type: Object, + required: true, }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 2122d0a508e..de3c7a80365 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -109,7 +109,7 @@ export default { class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded" > - <Icon + <icon :name="toggleIcon" :size="8" class="append-right-5" diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue index 7947ae1e4da..bf736a378dd 100644 --- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue +++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue @@ -1,90 +1,90 @@ <script> - export default { - props: { - startSize: { - type: Number, - required: true, - }, - side: { - type: String, - required: true, - }, - minSize: { - type: Number, - required: false, - default: 0, - }, - maxSize: { - type: Number, - required: false, - default: Number.MAX_VALUE, - }, - enabled: { - type: Boolean, - required: false, - default: true, - }, +export default { + props: { + startSize: { + type: Number, + required: true, }, - data() { - return { - size: this.startSize, - }; + side: { + type: String, + required: true, }, - computed: { - className() { - return `drag-${this.side}`; - }, - cursorStyle() { - if (this.enabled) { - return { cursor: 'ew-resize' }; - } - return {}; - }, + minSize: { + type: Number, + required: false, + default: 0, }, - methods: { - resetSize(e) { - e.preventDefault(); - this.$emit('resize-start', this.size); + maxSize: { + type: Number, + required: false, + default: Number.MAX_VALUE, + }, + enabled: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + return { + size: this.startSize, + }; + }, + computed: { + className() { + return `drag-${this.side}`; + }, + cursorStyle() { + if (this.enabled) { + return { cursor: 'ew-resize' }; + } + return {}; + }, + }, + methods: { + resetSize(e) { + e.preventDefault(); + this.$emit('resize-start', this.size); - this.size = this.startSize; - this.$emit('update:size', this.size); + this.size = this.startSize; + this.$emit('update:size', this.size); - // End resizing on next tick so that listeners can react to DOM changes - this.$nextTick(() => { - this.$emit('resize-end', this.size); - }); - }, - startDrag(e) { - if (this.enabled) { - e.preventDefault(); - this.startPos = e.clientX; - this.currentStartSize = this.size; - document.addEventListener('mousemove', this.drag); - document.addEventListener('mouseup', this.endDrag, { once: true }); - this.$emit('resize-start', this.size); - } - }, - drag(e) { + // End resizing on next tick so that listeners can react to DOM changes + this.$nextTick(() => { + this.$emit('resize-end', this.size); + }); + }, + startDrag(e) { + if (this.enabled) { e.preventDefault(); - let moved = e.clientX - this.startPos; - if (this.side === 'left') moved = -moved; - let newSize = this.currentStartSize + moved; - if (newSize < this.minSize) { - newSize = this.minSize; - } else if (newSize > this.maxSize) { - newSize = this.maxSize; - } - this.size = newSize; + this.startPos = e.clientX; + this.currentStartSize = this.size; + document.addEventListener('mousemove', this.drag); + document.addEventListener('mouseup', this.endDrag, { once: true }); + this.$emit('resize-start', this.size); + } + }, + drag(e) { + e.preventDefault(); + let moved = e.clientX - this.startPos; + if (this.side === 'left') moved = -moved; + let newSize = this.currentStartSize + moved; + if (newSize < this.minSize) { + newSize = this.minSize; + } else if (newSize > this.maxSize) { + newSize = this.maxSize; + } + this.size = newSize; - this.$emit('update:size', newSize); - }, - endDrag(e) { - e.preventDefault(); - document.removeEventListener('mousemove', this.drag); - this.$emit('resize-end', this.size); - }, + this.$emit('update:size', newSize); + }, + endDrag(e) { + e.preventDefault(); + document.removeEventListener('mousemove', this.drag); + this.$emit('resize-end', this.size); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue index bfeece12077..782d8e3abf6 100644 --- a/app/assets/javascripts/vue_shared/components/pikaday.vue +++ b/app/assets/javascripts/vue_shared/components/pikaday.vue @@ -1,62 +1,62 @@ <script> - import Pikaday from 'pikaday'; - import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix'; +import Pikaday from 'pikaday'; +import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix'; - export default { - name: 'DatePicker', - props: { - label: { - type: String, - required: false, - default: 'Date picker', - }, - selectedDate: { - type: Date, - required: false, - default: null, - }, - minDate: { - type: Date, - required: false, - default: null, - }, - maxDate: { - type: Date, - required: false, - default: null, - }, +export default { + name: 'DatePicker', + props: { + label: { + type: String, + required: false, + default: 'Date picker', }, - mounted() { - this.calendar = new Pikaday({ - field: this.$el.querySelector('.dropdown-menu-toggle'), - theme: 'gitlab-theme animate-picker', - format: 'yyyy-mm-dd', - container: this.$el, - defaultDate: this.selectedDate, - setDefaultDate: !!this.selectedDate, - minDate: this.minDate, - maxDate: this.maxDate, - parse: dateString => parsePikadayDate(dateString), - toString: date => pikadayToString(date), - onSelect: this.selected.bind(this), - onClose: this.toggled.bind(this), - }); - - this.$el.append(this.calendar.el); - this.calendar.show(); + selectedDate: { + type: Date, + required: false, + default: null, + }, + minDate: { + type: Date, + required: false, + default: null, }, - beforeDestroy() { - this.calendar.destroy(); + maxDate: { + type: Date, + required: false, + default: null, + }, + }, + mounted() { + this.calendar = new Pikaday({ + field: this.$el.querySelector('.dropdown-menu-toggle'), + theme: 'gitlab-theme animate-picker', + format: 'yyyy-mm-dd', + container: this.$el, + defaultDate: this.selectedDate, + setDefaultDate: !!this.selectedDate, + minDate: this.minDate, + maxDate: this.maxDate, + parse: dateString => parsePikadayDate(dateString), + toString: date => pikadayToString(date), + onSelect: this.selected.bind(this), + onClose: this.toggled.bind(this), + }); + + this.$el.append(this.calendar.el); + this.calendar.show(); + }, + beforeDestroy() { + this.calendar.destroy(); + }, + methods: { + selected(dateText) { + this.$emit('newDateSelected', this.calendar.toString(dateText)); }, - methods: { - selected(dateText) { - this.$emit('newDateSelected', this.calendar.toString(dateText)); - }, - toggled() { - this.$emit('hidePicker'); - }, + toggled() { + this.$emit('hidePicker'); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue index 97ca4d93bd7..1a2fd2ad985 100644 --- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue +++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue @@ -1,6 +1,5 @@ <script> - - /* This is a re-usable vue component for rendering a project avatar that +/* This is a re-usable vue component for rendering a project avatar that does not need to link to the project's profile. The image and an optional tooltip can be configured by props passed to this component. @@ -16,70 +15,70 @@ */ - import defaultAvatarUrl from 'images/no_avatar.png'; - import { placeholderImage } from '../../../lazy_loader'; - import tooltip from '../../directives/tooltip'; +import defaultAvatarUrl from 'images/no_avatar.png'; +import { placeholderImage } from '../../../lazy_loader'; +import tooltip from '../../directives/tooltip'; - export default { - name: 'ProjectAvatarImage', - directives: { - tooltip, +export default { + name: 'ProjectAvatarImage', + directives: { + tooltip, + }, + props: { + lazy: { + type: Boolean, + required: false, + default: false, + }, + imgSrc: { + type: String, + required: false, + default: defaultAvatarUrl, + }, + cssClasses: { + type: String, + required: false, + default: '', + }, + imgAlt: { + type: String, + required: false, + default: 'project avatar', + }, + size: { + type: Number, + required: false, + default: 20, + }, + tooltipText: { + type: String, + required: false, + default: '', + }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, + }, + computed: { + // API response sends null when gravatar is disabled and + // we provide an empty string when we use it inside project avatar link. + // In both cases we should render the defaultAvatarUrl + sanitizedSource() { + return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; + }, + resultantSrcAttribute() { + return this.lazy ? placeholderImage : this.sanitizedSource; }, - props: { - lazy: { - type: Boolean, - required: false, - default: false, - }, - imgSrc: { - type: String, - required: false, - default: defaultAvatarUrl, - }, - cssClasses: { - type: String, - required: false, - default: '', - }, - imgAlt: { - type: String, - required: false, - default: 'project avatar', - }, - size: { - type: Number, - required: false, - default: 20, - }, - tooltipText: { - type: String, - required: false, - default: '', - }, - tooltipPlacement: { - type: String, - required: false, - default: 'top', - }, + tooltipContainer() { + return this.tooltipText ? 'body' : null; }, - computed: { - // API response sends null when gravatar is disabled and - // we provide an empty string when we use it inside project avatar link. - // In both cases we should render the defaultAvatarUrl - sanitizedSource() { - return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; - }, - resultantSrcAttribute() { - return this.lazy ? placeholderImage : this.sanitizedSource; - }, - tooltipContainer() { - return this.tooltipText ? 'body' : null; - }, - avatarSizeClass() { - return `s${this.size}`; - }, + avatarSizeClass() { + return `s${this.size}`; }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index a2a9a5e6987..09394847b10 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -1,67 +1,67 @@ <script> - import DeprecatedModal from './deprecated_modal.vue'; +import DeprecatedModal from './deprecated_modal.vue'; - export default { - name: 'RecaptchaModal', +export default { + name: 'RecaptchaModal', - components: { - DeprecatedModal, - }, + components: { + DeprecatedModal, + }, - props: { - html: { - type: String, - required: false, - default: '', - }, + props: { + html: { + type: String, + required: false, + default: '', }, + }, - data() { - return { - script: {}, - scriptSrc: 'https://www.google.com/recaptcha/api.js', - }; - }, + data() { + return { + script: {}, + scriptSrc: 'https://www.google.com/recaptcha/api.js', + }; + }, - watch: { - html() { - this.appendRecaptchaScript(); - }, + watch: { + html() { + this.appendRecaptchaScript(); }, + }, - mounted() { - window.recaptchaDialogCallback = this.submit.bind(this); - }, + mounted() { + window.recaptchaDialogCallback = this.submit.bind(this); + }, - methods: { - appendRecaptchaScript() { - this.removeRecaptchaScript(); + methods: { + appendRecaptchaScript() { + this.removeRecaptchaScript(); - const script = document.createElement('script'); - script.src = this.scriptSrc; - script.classList.add('js-recaptcha-script'); - script.async = true; - script.defer = true; + const script = document.createElement('script'); + script.src = this.scriptSrc; + script.classList.add('js-recaptcha-script'); + script.async = true; + script.defer = true; - this.script = script; + this.script = script; - document.body.appendChild(script); - }, + document.body.appendChild(script); + }, - removeRecaptchaScript() { - if (this.script instanceof Element) this.script.remove(); - }, + removeRecaptchaScript() { + if (this.script instanceof Element) this.script.remove(); + }, - close() { - this.removeRecaptchaScript(); - this.$emit('close'); - }, + close() { + this.removeRecaptchaScript(); + this.$emit('close'); + }, - submit() { - this.$el.querySelector('form').submit(); - }, + submit() { + this.$el.querySelector('form').submit(); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 9d757b27edc..500586302cf 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -1,96 +1,96 @@ <script> - import datePicker from '../pikaday.vue'; - import toggleSidebar from './toggle_sidebar.vue'; - import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; - import { dateInWords } from '../../../lib/utils/datetime_utility'; +import datePicker from '../pikaday.vue'; +import toggleSidebar from './toggle_sidebar.vue'; +import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; +import { dateInWords } from '../../../lib/utils/datetime_utility'; - export default { - name: 'SidebarDatePicker', - components: { - datePicker, - toggleSidebar, - collapsedCalendarIcon, - }, - props: { - blockClass: { - type: String, - required: false, - default: '', - }, - collapsed: { - type: Boolean, - required: false, - default: true, - }, - showToggleSidebar: { - type: Boolean, - required: false, - default: false, - }, - isLoading: { - type: Boolean, - required: false, - default: false, - }, - editable: { - type: Boolean, - required: false, - default: false, - }, - label: { - type: String, - required: false, - default: 'Date picker', - }, - selectedDate: { - type: Date, - required: false, - default: null, - }, - minDate: { - type: Date, - required: false, - default: null, - }, - maxDate: { - type: Date, - required: false, - default: null, - }, - }, - data() { - return { - editing: false, - }; - }, - computed: { - selectedAndEditable() { - return this.selectedDate && this.editable; - }, - selectedDateWords() { - return dateInWords(this.selectedDate, true); - }, - collapsedText() { - return this.selectedDateWords ? this.selectedDateWords : 'None'; - }, - }, - methods: { - stopEditing() { - this.editing = false; - }, - toggleDatePicker() { - this.editing = !this.editing; - }, - newDateSelected(date = null) { - this.date = date; - this.editing = false; - this.$emit('saveDate', date); - }, - toggleSidebar() { - this.$emit('toggleCollapse'); - }, - }, - }; +export default { + name: 'SidebarDatePicker', + components: { + datePicker, + toggleSidebar, + collapsedCalendarIcon, + }, + props: { + blockClass: { + type: String, + required: false, + default: '', + }, + collapsed: { + type: Boolean, + required: false, + default: true, + }, + showToggleSidebar: { + type: Boolean, + required: false, + default: false, + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + editable: { + type: Boolean, + required: false, + default: false, + }, + label: { + type: String, + required: false, + default: 'Date picker', + }, + selectedDate: { + type: Date, + required: false, + default: null, + }, + minDate: { + type: Date, + required: false, + default: null, + }, + maxDate: { + type: Date, + required: false, + default: null, + }, + }, + data() { + return { + editing: false, + }; + }, + computed: { + selectedAndEditable() { + return this.selectedDate && this.editable; + }, + selectedDateWords() { + return dateInWords(this.selectedDate, true); + }, + collapsedText() { + return this.selectedDateWords ? this.selectedDateWords : 'None'; + }, + }, + methods: { + stopEditing() { + this.editing = false; + }, + toggleDatePicker() { + this.editing = !this.editing; + }, + newDateSelected(date = null) { + this.date = date; + this.editing = false; + this.$emit('saveDate', date); + }, + toggleSidebar() { + this.$emit('toggleCollapse'); + }, + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 8e9621c956f..03a5a078879 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -1,17 +1,17 @@ <script> - import { s__ } from '../../locale'; - - const PAGINATION_UI_BUTTON_LIMIT = 4; - const UI_LIMIT = 6; - const SPREAD = '...'; - const PREV = s__('Pagination|Prev'); - const NEXT = s__('Pagination|Next'); - const FIRST = s__('Pagination|« First'); - const LAST = s__('Pagination|Last »'); - - export default { - props: { - /** +import { s__ } from '../../locale'; + +const PAGINATION_UI_BUTTON_LIMIT = 4; +const UI_LIMIT = 6; +const SPREAD = '...'; +const PREV = s__('Pagination|Prev'); +const NEXT = s__('Pagination|Next'); +const FIRST = s__('Pagination|« First'); +const LAST = s__('Pagination|Last »'); + +export default { + props: { + /** This function will take the information given by the pagination component Here is an example `change` method: @@ -20,12 +20,12 @@ gl.utils.visitUrl(`?page=${pagenum}`); }, */ - change: { - type: Function, - required: true, - }, + change: { + type: Function, + required: true, + }, - /** + /** pageInfo will come from the headers of the API call in the `.then` clause of the VueResource API call there should be a function that contructs the pageInfo for this component @@ -41,94 +41,94 @@ previousPage: +headers['X-Prev-Page'], }); */ - pageInfo: { - type: Object, - required: true, - }, + pageInfo: { + type: Object, + required: true, + }, + }, + computed: { + prev() { + return this.pageInfo.previousPage; + }, + next() { + return this.pageInfo.nextPage; + }, + getItems() { + const total = this.pageInfo.totalPages; + const { page } = this.pageInfo; + const items = []; + + if (page > 1) { + items.push({ title: FIRST, first: true }); + } + + if (page > 1) { + items.push({ title: PREV, prev: true }); + } else { + items.push({ title: PREV, disabled: true, prev: true }); + } + + if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); + + const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); + const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); + + for (let i = start; i <= end; i += 1) { + const isActive = i === page; + items.push({ title: i, active: isActive, page: true }); + } + + if (total - page > PAGINATION_UI_BUTTON_LIMIT) { + items.push({ title: SPREAD, separator: true, page: true }); + } + + if (page === total) { + items.push({ title: NEXT, disabled: true, next: true }); + } else if (total - page >= 1) { + items.push({ title: NEXT, next: true }); + } + + if (total - page >= 1) { + items.push({ title: LAST, last: true }); + } + + return items; + }, + showPagination() { + return this.pageInfo.totalPages > 1; }, - computed: { - prev() { - return this.pageInfo.previousPage; - }, - next() { - return this.pageInfo.nextPage; - }, - getItems() { - const total = this.pageInfo.totalPages; - const { page } = this.pageInfo; - const items = []; - - if (page > 1) { - items.push({ title: FIRST, first: true }); - } - - if (page > 1) { - items.push({ title: PREV, prev: true }); - } else { - items.push({ title: PREV, disabled: true, prev: true }); - } - - if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); - - const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); - const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); - - for (let i = start; i <= end; i += 1) { - const isActive = i === page; - items.push({ title: i, active: isActive, page: true }); - } - - if (total - page > PAGINATION_UI_BUTTON_LIMIT) { - items.push({ title: SPREAD, separator: true, page: true }); - } - - if (page === total) { - items.push({ title: NEXT, disabled: true, next: true }); - } else if (total - page >= 1) { - items.push({ title: NEXT, next: true }); - } - - if (total - page >= 1) { - items.push({ title: LAST, last: true }); - } - - return items; - }, - showPagination() { - return this.pageInfo.totalPages > 1; - }, + }, + methods: { + changePage(text, isDisabled) { + if (isDisabled) return; + + const { totalPages, nextPage, previousPage } = this.pageInfo; + + switch (text) { + case SPREAD: + break; + case LAST: + this.change(totalPages); + break; + case NEXT: + this.change(nextPage); + break; + case PREV: + this.change(previousPage); + break; + case FIRST: + this.change(1); + break; + default: + this.change(+text); + break; + } }, - methods: { - changePage(text, isDisabled) { - if (isDisabled) return; - - const { totalPages, nextPage, previousPage } = this.pageInfo; - - switch (text) { - case SPREAD: - break; - case LAST: - this.change(totalPages); - break; - case NEXT: - this.change(nextPage); - break; - case PREV: - this.change(previousPage); - break; - case FIRST: - this.change(1); - break; - default: - this.change(+text); - break; - } - }, - hideOnSmallScreen(item) { - return !item.first && !item.last && !item.next && !item.prev && !item.active; - }, + hideOnSmallScreen(item) { + return !item.first && !item.last && !item.next && !item.prev && !item.active; }, - }; + }, +}; </script> <template> <div diff --git a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue index 368eeb6c453..d760263929a 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -11,9 +11,7 @@ export default { directives: { tooltip, }, - mixins: [ - timeagoMixin, - ], + mixins: [timeagoMixin], props: { time: { type: String, diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue index 5b9c51786d6..4e9289cbed8 100644 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue @@ -1,60 +1,60 @@ <script> - import { s__ } from '../../locale'; - import icon from './icon.vue'; +import { s__ } from '../../locale'; +import icon from './icon.vue'; - const ICON_ON = 'status_success_borderless'; - const ICON_OFF = 'status_failed_borderless'; - const LABEL_ON = s__('ToggleButton|Toggle Status: ON'); - const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF'); +const ICON_ON = 'status_success_borderless'; +const ICON_OFF = 'status_failed_borderless'; +const LABEL_ON = s__('ToggleButton|Toggle Status: ON'); +const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF'); - export default { - components: { - icon, - }, +export default { + components: { + icon, + }, - model: { - prop: 'value', - event: 'change', - }, + model: { + prop: 'value', + event: 'change', + }, - props: { - name: { - type: String, - required: false, - default: null, - }, - value: { - type: Boolean, - required: false, - default: null, - }, - disabledInput: { - type: Boolean, - required: false, - default: false, - }, - isLoading: { - type: Boolean, - required: false, - default: false, - }, + props: { + name: { + type: String, + required: false, + default: null, + }, + value: { + type: Boolean, + required: false, + default: null, }, + disabledInput: { + type: Boolean, + required: false, + default: false, + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + }, - computed: { - toggleIcon() { - return this.value ? ICON_ON : ICON_OFF; - }, - ariaLabel() { - return this.value ? LABEL_ON : LABEL_OFF; - }, + computed: { + toggleIcon() { + return this.value ? ICON_ON : ICON_OFF; + }, + ariaLabel() { + return this.value ? LABEL_ON : LABEL_OFF; }, + }, - methods: { - toggleFeature() { - if (!this.disabledInput) this.$emit('change', !this.value); - }, + methods: { + toggleFeature() { + if (!this.disabledInput) this.$emit('change', !this.value); }, - }; + }, +}; </script> <template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index ee3157bcb1b..14cb44b8619 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -1,5 +1,4 @@ <script> - /* This is a re-usable vue component for rendering a user avatar wrapped in a clickable link (likely to the user's profile). The link, image, and tooltip can be configured by props passed to this component. diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue index ef3b16edf5f..8e460566d09 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue @@ -1,5 +1,4 @@ <script> - /* This is a re-usable vue component for rendering a user avatar svg (typically for a blank state). It will receive styles comparable to the user avatar, but no image is loaded, it isn't wrapped in a link, and tooltips aren't supported. @@ -42,4 +41,3 @@ export default { v-html="svg" /> </template> - diff --git a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js index f94cc670edf..67a1632269e 100644 --- a/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js +++ b/app/assets/javascripts/vue_shared/mixins/ci_pagination_api_mixin.js @@ -4,10 +4,7 @@ * * Components need to have `scope`, `page` and `requestData` */ -import { - historyPushState, - buildUrlWithCurrentLocation, -} from '../../lib/utils/common_utils'; +import { historyPushState, buildUrlWithCurrentLocation } from '../../lib/utils/common_utils'; export default { methods: { @@ -24,12 +21,14 @@ export default { // stop polling this.poll.stop(); - const queryString = Object.keys(parameters).map((parameter) => { - const value = parameters[parameter]; - // update internal state for UI - this[parameter] = value; - return `${parameter}=${encodeURIComponent(value)}`; - }).join('&'); + const queryString = Object.keys(parameters) + .map(parameter => { + const value = parameters[parameter]; + // update internal state for UI + this[parameter] = value; + return `${parameter}=${encodeURIComponent(value)}`; + }) + .join('&'); // update polling parameters this.requestData = parameters; diff --git a/app/assets/javascripts/vue_shared/models/label.js b/app/assets/javascripts/vue_shared/models/label.js index d29c7fe973a..2d2732d0661 100644 --- a/app/assets/javascripts/vue_shared/models/label.js +++ b/app/assets/javascripts/vue_shared/models/label.js @@ -6,7 +6,7 @@ export default class ListLabel { this.color = obj.color; this.textColor = obj.text_color; this.description = obj.description; - this.priority = (obj.priority !== null) ? obj.priority : Infinity; + this.priority = obj.priority !== null ? obj.priority : Infinity; } } diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js index 48c63373b77..e0baf03acc3 100644 --- a/app/assets/javascripts/vue_shared/translate.js +++ b/app/assets/javascripts/vue_shared/translate.js @@ -1,11 +1,6 @@ -import { - __, - n__, - s__, - sprintf, -} from '../locale'; +import { __, n__, s__, sprintf } from '../locale'; -export default (Vue) => { +export default Vue => { Vue.mixin({ methods: { /** diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index b9693892f45..754025207c8 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -21,7 +21,7 @@ Vue.http.interceptors.push((request, next) => { Vue.http.interceptors.push((request, next) => { request.headers.set(csrf.headerKey, csrf.token); - next((response) => { + next(response => { // Headers object has a `forEach` property that iterates through all values. const headers = {}; diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index bdb2351c344..e98c4d7bf7a 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -47,16 +47,26 @@ export default class ZenMode { e.preventDefault(); return $(e.currentTarget).trigger('zen_mode:leave'); }); - $(document).on('zen_mode:enter', (function(_this) { - return function(e) { - return _this.enter($(e.target).closest('.md-area').find('.zen-backdrop')); - }; - })(this)); - $(document).on('zen_mode:leave', (function(_this) { - return function(e) { - return _this.exit(); - }; - })(this)); + $(document).on( + 'zen_mode:enter', + (function(_this) { + return function(e) { + return _this.enter( + $(e.target) + .closest('.md-area') + .find('.zen-backdrop'), + ); + }; + })(this), + ); + $(document).on( + 'zen_mode:leave', + (function(_this) { + return function(e) { + return _this.exit(); + }; + })(this), + ); $(document).on('keydown', function(e) { // Esc if (e.keyCode === 27) { @@ -93,7 +103,7 @@ export default class ZenMode { scrollTo(zen_area) { return $.scrollTo(zen_area, 0, { - offset: -150 + offset: -150, }); } } diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 7145a76db6d..f26b1fddae5 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -283,18 +283,20 @@ .dismiss-button { position: absolute; - right: 6px; - top: 6px; + right: $gl-padding-8; + top: $gl-padding-8; cursor: pointer; - color: $blue-300; + color: $blue-500; z-index: 1; border: 0; background-color: transparent; + padding: $gl-padding-8; + line-height: 0; &:hover, &:focus { border: 0; - color: $blue-400; + color: $blue-700; } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 8603714f709..cdfad30e7ca 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -144,6 +144,13 @@ top: 11px; right: 8px; } + + .ic-chevron-down { + position: absolute; + top: $gl-padding-8; + right: $gl-padding-8; + color: $gray-darkest; + } } @mixin dropdown-item-hover { @@ -561,6 +568,10 @@ top: -1px; } +.dropdown-menu-close-icon { + vertical-align: middle; +} + .dropdown-menu-back { left: 7px; top: 2px; @@ -572,9 +583,10 @@ padding: 0 10px; .fa, - .input-icon { + .input-icon, + .ic-search { position: absolute; - top: 10px; + top: $gl-padding-8; right: 20px; color: $dropdown-input-fa-color; font-size: 12px; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index c430009bfe0..2bee2961847 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -530,8 +530,6 @@ .header-user { &.show .dropdown-menu { - width: auto; - min-width: unset; max-height: 323px; margin-top: 4px; color: $gl-text-color; @@ -548,7 +546,7 @@ margin-right: 0; display: block; vertical-align: text-top; - max-width: 148px; + max-width: 240px; font-size: 12px; gl-emoji { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b7a95f604b8..0fde6e18cc7 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -605,6 +605,7 @@ $perf-bar-development: #4c1210; $perf-bar-bucket-bg: #111; $perf-bar-bucket-box-shadow-from: rgba($white-light, 0.2); $perf-bar-bucket-box-shadow-to: rgba($black, 0.25); +$perf-bar-canary-text: $orange-400; /* Issuable warning diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index b3c5c693824..54fbd40cece 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -305,8 +305,7 @@ } .confidential-icon { - position: relative; - top: 1px; + vertical-align: text-top; margin-right: 5px; } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index ed877f625b5..227f49ec595 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -117,7 +117,6 @@ .controllers { display: flex; - font-size: 15px; justify-content: center; align-items: center; @@ -179,6 +178,7 @@ .build-loader-animation { @include build-loader-animation; + float: left; } } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index f0228768b5a..ec2108b15be 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -21,7 +21,7 @@ } } - svg { + .svg-container svg { width: 136px; height: 136px; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index cba5324ce53..8d884ad6891 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1013,6 +1013,7 @@ .with-performance-bar & { top: 135px; + max-height: calc(100vh - 135px); } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 79984c1a546..19a36061c45 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -90,6 +90,7 @@ margin-right: 3px; color: $gl-text-color-secondary; display: inline-block; + vertical-align: text-top; .fa:nth-child(1) { margin-right: 3px; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index fe792a53b44..f0cb81e0bc3 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -191,7 +191,7 @@ } } - svg { + .svg-container svg { width: 62px; height: 50px; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 45382d4ea43..895db89f289 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -201,7 +201,6 @@ .mr-widget-icon { font-size: 22px; - margin-right: $gl-btn-padding; } .ci-status-icon svg { diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 4268e194ed7..c60bb360a03 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -340,6 +340,8 @@ } .note-form-actions { + color: $gl-text-color; + @include media-breakpoint-down(xs) { .btn { float: none; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index dbf8692d69b..ccfa4e00a5b 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -42,6 +42,10 @@ margin-top: 0; } + .settings-title { + cursor: pointer; + } + button { position: absolute; top: 20px; diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 59fdbf31fe9..9c01a2f8bda 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -68,6 +68,10 @@ } } + .current-host.canary { + color: $perf-bar-canary-text; + } + strong { color: $white-light; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bbeaeb7694e..eeabcc0c9bb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,7 +12,9 @@ class ApplicationController < ActionController::Base include WorkhorseHelper include EnforcesTwoFactorAuthentication include WithPerformanceBar - include InvalidUTF8ErrorHandler + # this can be removed after switching to rails 5 + # https://gitlab.com/gitlab-org/gitlab-ce/issues/51908 + include InvalidUTF8ErrorHandler unless Gitlab.rails5? before_action :authenticate_sessionless_user! before_action :authenticate_user! diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 3766b64a091..0d5c8657c9e 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -20,7 +20,7 @@ class AutocompleteController < ApplicationController end def user - user = UserFinder.new(params).execute! + user = UserFinder.new(params[:id]).find_by_id! render json: UserSerializer.new.represent(user) end diff --git a/app/controllers/concerns/sends_blob.rb b/app/controllers/concerns/sends_blob.rb index 971390d9118..8ecdaced9f5 100644 --- a/app/controllers/concerns/sends_blob.rb +++ b/app/controllers/concerns/sends_blob.rb @@ -8,7 +8,7 @@ module SendsBlob include SendFileUpload end - def send_blob(blob, params = {}) + def send_blob(repository, blob, params = {}) if blob headers['X-Content-Type-Options'] = 'nosniff' diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 1dfa814cdd5..e3eec5a020d 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -20,7 +20,7 @@ class Import::GithubController < Import::BaseController end def personal_access_token - session[access_token_key] = params[:personal_access_token] + session[access_token_key] = params[:personal_access_token]&.strip redirect_to status_import_url end diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb deleted file mode 100644 index 72aa9d4f17f..00000000000 --- a/app/controllers/koding_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class KodingController < ApplicationController - before_action :check_integration! - layout 'koding' - - def index - path = File.join(Rails.root, 'doc/user/project/koding.md') - @markdown = File.read(path) - end - - private - - def check_integration! - render_404 unless Gitlab::CurrentSettings.koding_enabled? - end -end diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 01801c31327..3c3dc03a4ee 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -38,7 +38,7 @@ class Profiles::KeysController < Profiles::ApplicationController def get_keys if params[:username].present? begin - user = User.find_by_username(params[:username]) + user = UserFinder.new(params[:username]).find_by_username if user.present? render text: user.all_ssh_keys.join("\n"), content_type: "text/plain" else diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index d0f59aa8162..312e256ea6c 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -10,7 +10,7 @@ class Projects::ArtifactsController < Projects::ApplicationController before_action :authorize_update_build!, only: [:keep] before_action :extract_ref_name_and_path before_action :set_request_format, only: [:file] - before_action :validate_artifacts! + before_action :validate_artifacts!, except: [:download] before_action :entry, only: [:file] def download @@ -102,7 +102,7 @@ class Projects::ArtifactsController < Projects::ApplicationController # rubocop: enable CodeReuse/ActiveRecord def artifacts_file - @artifacts_file ||= build.artifacts_file_for_type(params[:file_type] || :archive) + @artifacts_file ||= build&.artifacts_file_for_type(params[:file_type] || :archive) end def entry diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 1c385c0e15a..1f4a25f82e9 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -8,7 +8,7 @@ class Projects::AvatarsController < Projects::ApplicationController def show @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git) - send_blob(@blob) + send_blob(@repository, @blob) end def destroy diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 92d26a13da9..fb2cfdedd9b 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -83,7 +83,7 @@ class Projects::BlobController < Projects::ApplicationController def destroy create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.", - success_path: -> { project_tree_path(@project, @branch_name) }, + success_path: -> { after_delete_path }, failure_view: :show, failure_path: project_blob_path(@project, @id)) end @@ -191,6 +191,15 @@ class Projects::BlobController < Projects::ApplicationController end # rubocop: enable CodeReuse/ActiveRecord + def after_delete_path + branch = BranchesFinder.new(@repository, search: @ref).execute.first + if @repository.tree(branch.target, tree_path).entries.empty? + project_tree_path(@project, @ref) + else + project_tree_path(@project, File.join(@ref, tree_path)) + end + end + def editor_variables @branch_name = params[:branch_name] @@ -255,9 +264,6 @@ class Projects::BlobController < Projects::ApplicationController def show_json set_last_commit_sha - path_segments = @path.split('/') - path_segments.pop - tree_path = path_segments.join('/') json = { id: @blob.id, @@ -283,4 +289,8 @@ class Projects::BlobController < Projects::ApplicationController render json: json end + + def tree_path + @path.rpartition('/').first + end end diff --git a/app/controllers/projects/build_artifacts_controller.rb b/app/controllers/projects/build_artifacts_controller.rb index 46449a4aae9..7d4d566499c 100644 --- a/app/controllers/projects/build_artifacts_controller.rb +++ b/app/controllers/projects/build_artifacts_controller.rb @@ -6,10 +6,10 @@ class Projects::BuildArtifactsController < Projects::ApplicationController before_action :authorize_read_build! before_action :extract_ref_name_and_path - before_action :validate_artifacts! + before_action :validate_artifacts!, except: [:download] def download - redirect_to download_project_job_artifacts_path(project, job) + redirect_to download_project_job_artifacts_path(project, job, params: request.query_parameters) end def browse diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index c356f8d2987..bcea96bce94 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -2,31 +2,20 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationController before_action :cluster - before_action :application_class, only: [:create] before_action :authorize_read_cluster! before_action :authorize_create_cluster!, only: [:create] - # rubocop: disable CodeReuse/ActiveRecord def create - application = @application_class.find_or_initialize_by(cluster: @cluster) - - if application.has_attribute?(:hostname) - application.hostname = params[:hostname] - end - - if application.respond_to?(:oauth_application) - application.oauth_application = create_oauth_application(application) - end - - application.save! - - Clusters::Applications::ScheduleInstallationService.new(project, current_user).execute(application) + Clusters::Applications::CreateService + .new(@cluster, current_user, create_cluster_application_params) + .execute(request) head :no_content + rescue Clusters::Applications::CreateService::InvalidApplicationError + render_404 rescue StandardError head :bad_request end - # rubocop: enable CodeReuse/ActiveRecord private @@ -34,18 +23,7 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll @cluster ||= project.clusters.find(params[:id]) || render_404 end - def application_class - @application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404 - end - - def create_oauth_application(application) - oauth_application_params = { - name: params[:application], - redirect_uri: application.callback_url, - scopes: 'api read_user openid', - owner: current_user - } - - Applications::CreateService.new(current_user, oauth_application_params).execute(request) + def create_cluster_application_params + params.permit(:application, :hostname) end end diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index bcdbf48bb35..62adc66fb09 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -40,7 +40,7 @@ class Projects::ClustersController < Projects::ApplicationController def update Clusters::UpdateService - .new(project, current_user, update_params) + .new(current_user, update_params) .execute(cluster) if cluster.valid? @@ -73,8 +73,8 @@ class Projects::ClustersController < Projects::ApplicationController def create_gcp @gcp_cluster = ::Clusters::CreateService - .new(project, current_user, create_gcp_cluster_params) - .execute(token_in_session) + .new(current_user, create_gcp_cluster_params) + .execute(project: project, access_token: token_in_session) if @gcp_cluster.persisted? redirect_to project_cluster_path(project, @gcp_cluster) @@ -89,8 +89,8 @@ class Projects::ClustersController < Projects::ApplicationController def create_user @user_cluster = ::Clusters::CreateService - .new(project, current_user, create_user_cluster_params) - .execute(token_in_session) + .new(current_user, create_user_cluster_params) + .execute(project: project, access_token: token_in_session) if @user_cluster.persisted? redirect_to project_cluster_path(project, @user_cluster) diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 1dd5d1ff2e8..42ae5b0ef3c 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -12,6 +12,6 @@ class Projects::RawController < Projects::ApplicationController def show @blob = @repository.blob_at(@commit.id, @path) - send_blob(@blob, inline: (params[:inline] != 'false')) + send_blob(@repository, @blob, inline: (params[:inline] != 'false')) end end diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 8c6d87a421f..88dd111132b 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -2,6 +2,7 @@ class Projects::WikisController < Projects::ApplicationController include PreviewMarkdown + include SendsBlob include Gitlab::Utils::StrongMemoize before_action :authorize_read_wiki! @@ -26,16 +27,8 @@ class Projects::WikisController < Projects::ApplicationController set_encoding_error unless valid_encoding? render 'show' - elsif file = @project_wiki.find_file(params[:id], params[:version_id]) - response.headers['Content-Security-Policy'] = "default-src 'none'" - response.headers['X-Content-Security-Policy'] = "default-src 'none'" - - send_data( - file.raw_data, - type: file.mime_type, - disposition: 'inline', - filename: file.name - ) + elsif file_blob + send_blob(@project_wiki.repository, file_blob) elsif can?(current_user, :create_wiki, @project) && view_param == 'create' @page = build_page(title: params[:id]) @@ -164,4 +157,14 @@ class Projects::WikisController < Projects::ApplicationController def set_encoding_error flash.now[:notice] = "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository." end + + def file_blob + strong_memoize(:file_blob) do + commit = @project_wiki.repository.commit(@project_wiki.default_branch) + + next unless commit + + @project_wiki.repository.blob_at(commit.id, params[:id]) + end + end end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 694c3a59e2b..dd9bf17cf0c 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -26,12 +26,9 @@ class SnippetsController < ApplicationController layout 'snippets' respond_to :html - # rubocop: disable CodeReuse/ActiveRecord def index if params[:username].present? - @user = User.find_by(username: params[:username]) - - return render_404 unless @user + @user = UserFinder.new(params[:username]).find_by_username! @snippets = SnippetsFinder.new(current_user, author: @user, scope: params[:scope]) .execute.page(params[:page]) @@ -41,7 +38,6 @@ class SnippetsController < ApplicationController redirect_to(current_user ? dashboard_snippets_path : explore_snippets_path) end end - # rubocop: enable CodeReuse/ActiveRecord def new @snippet = PersonalSnippet.new diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d16240af404..5b70c69d7f4 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -56,10 +56,12 @@ class UsersController < ApplicationController def projects load_projects + skip_pagination = Gitlab::Utils.to_boolean(params[:skip_pagination]) + respond_to do |format| format.html { render 'show' } format.json do - pager_json("shared/projects/_list", @projects.count, projects: @projects) + pager_json("shared/projects/_list", @projects.count, projects: @projects, skip_pagination: skip_pagination) end end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 1f98ecf95ca..8abfe0c4c17 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -256,7 +256,7 @@ class IssuableFinder if assignee_id? User.find_by(id: params[:assignee_id]) elsif assignee_username? - User.find_by(username: params[:assignee_username]) + User.find_by_username(params[:assignee_username]) else nil end @@ -284,7 +284,7 @@ class IssuableFinder if author_id? User.find_by(id: params[:author_id]) elsif author_username? - User.find_by(username: params[:author_username]) + User.find_by_username(params[:author_username]) else nil end diff --git a/app/finders/user_finder.rb b/app/finders/user_finder.rb index 815388c894e..556be4c4338 100644 --- a/app/finders/user_finder.rb +++ b/app/finders/user_finder.rb @@ -7,22 +7,52 @@ # times we may want to exclude blocked user. By using this finder (and extending # it whenever necessary) we can keep this logic in one place. class UserFinder - attr_reader :params + def initialize(username_or_id) + @username_or_id = username_or_id + end + + # Tries to find a User by id, returning nil if none could be found. + def find_by_id + User.find_by_id(@username_or_id) + end - def initialize(params) - @params = params + # Tries to find a User by id, raising a `ActiveRecord::RecordNotFound` if it could + # not be found. + def find_by_id! + User.find(@username_or_id) end - # Tries to find a User, returning nil if none could be found. - # rubocop: disable CodeReuse/ActiveRecord - def execute - User.find_by(id: params[:id]) + # Tries to find a User by username, returning nil if none could be found. + def find_by_username + User.find_by_username(@username_or_id) end - # rubocop: enable CodeReuse/ActiveRecord - # Tries to find a User, raising a `ActiveRecord::RecordNotFound` if it could + # Tries to find a User by username, raising a `ActiveRecord::RecordNotFound` if it could # not be found. - def execute! - User.find(params[:id]) + def find_by_username! + User.find_by_username!(@username_or_id) + end + + # Tries to find a User by username or id, returning nil if none could be found. + def find_by_id_or_username + if input_is_id? + find_by_id + else + find_by_username + end + end + + # Tries to find a User by username or id, raising a `ActiveRecord::RecordNotFound` if it could + # not be found. + def find_by_id_or_username! + if input_is_id? + find_by_id! + else + find_by_username! + end + end + + def input_is_id? + @username_or_id.is_a?(Numeric) || @username_or_id =~ /^\d+$/ end end diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index f2ad9b4bda5..81ae50c0bd1 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -43,13 +43,11 @@ class UsersFinder private - # rubocop: disable CodeReuse/ActiveRecord def by_username(users) return users unless params[:username] - users.where(username: params[:username]) + users.by_username(params[:username]) end - # rubocop: enable CodeReuse/ActiveRecord def by_search(users) return users unless params[:search].present? diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index d6753e46165..0c9f69b6714 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -7,7 +7,6 @@ module ApplicationSettingsHelper :gravatar_enabled?, :password_authentication_enabled_for_web?, :akismet_enabled?, - :koding_enabled?, to: :'Gitlab::CurrentSettings.current_application_settings' def user_oauth_applications? @@ -155,8 +154,6 @@ module ApplicationSettingsHelper :housekeeping_incremental_repack_period, :html_emails_enabled, :import_sources, - :koding_enabled, - :koding_url, :max_artifacts_size, :max_attachment_size, :max_pages_size, diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 883e5ddff57..f7e087a6234 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -150,7 +150,9 @@ module BlobHelper # example of Javascript) we tell the browser of the victim not to # execute untrusted data. def safe_content_type(blob) - if blob.text? + if blob.extension == 'svg' + blob.mime_type + elsif blob.text? 'text/plain; charset=utf-8' elsif blob.image? blob.content_type @@ -159,6 +161,12 @@ module BlobHelper end end + def content_disposition(blob, inline) + return 'attachment' if blob.extension == 'svg' + + inline ? 'inline' : 'attachment' + end + def ref_project @ref_project ||= @target_project || @project end @@ -195,7 +203,7 @@ module BlobHelper { 'relative-url-root' => Rails.application.config.relative_url_root, 'assets-prefix' => Gitlab::Application.config.assets.prefix, - 'blob-language' => @blob && @blob.language.try(:ace_mode), + 'blob-filename' => @blob && @blob.path, 'project-id' => project.id } end diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index 33c53021c11..463f4145bdd 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -21,29 +21,6 @@ module DashboardHelper links.any? { |link| dashboard_nav_link?(link) } end - def controller_action_to_child_dashboards(controller = controller_name, action = action_name) - case "#{controller}##{action}" - when 'projects#index', 'root#index', 'projects#starred', 'projects#trending' - %w(projects stars) - when 'dashboard#activity' - %w(starred_project_activity project_activity) - when 'groups#index' - %w(groups) - when 'todos#index' - %w(todos) - when 'dashboard#issues' - %w(issues) - when 'dashboard#merge_requests' - %w(merge_requests) - else - [] - end - end - - def user_default_dashboard?(user = current_user) - controller_action_to_child_dashboards.any? {|dashboard| dashboard == user.dashboard } - end - private def get_dashboard_nav_links diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 0016f89db5c..d9713f9c9b0 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -388,22 +388,6 @@ module ProjectsHelper end end - def koding_project_url(project = nil, branch = nil, sha = nil) - if project - import_path = "/Home/Stacks/import" - - repo = project.full_path - branch ||= project.default_branch - sha ||= project.commit.short_id - - path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}" - - return URI.join(Gitlab::CurrentSettings.koding_url, path).to_s - end - - Gitlab::CurrentSettings.koding_url - end - def project_wiki_path_with_version(proj, page, version, is_newest) url_params = is_newest ? {} : { version_id: version } project_wiki_path(proj, page, url_params) diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb index f19445fca1a..49c08dce96c 100644 --- a/app/helpers/workhorse_helper.rb +++ b/app/helpers/workhorse_helper.rb @@ -6,7 +6,7 @@ module WorkhorseHelper # Send a Git blob through Workhorse def send_git_blob(repository, blob, inline: true) headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob)) - headers['Content-Disposition'] = inline ? 'inline' : 'attachment' + headers['Content-Disposition'] = content_disposition(blob, inline) headers['Content-Type'] = safe_content_type(blob) render plain: "" end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index f7347ee61b4..662f3e00047 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -118,6 +118,7 @@ class Notify < BaseMailer add_unsubscription_headers_and_links headers["X-GitLab-#{model.class.name}-ID"] = model.id + headers["X-GitLab-#{model.class.name}-IID"] = model.iid if model.respond_to?(:iid) headers['X-GitLab-Reply-Key'] = reply_key @reason = headers['X-GitLab-NotificationReason'] diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 23131af1b7d..b66ec0ffab6 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -33,6 +33,8 @@ class ApplicationSetting < ActiveRecord::Base ignore_column :circuitbreaker_storage_timeout ignore_column :circuitbreaker_access_retries ignore_column :circuitbreaker_check_interval + ignore_column :koding_url + ignore_column :koding_enabled cache_markdown_field :sign_in_text cache_markdown_field :help_page_text @@ -100,10 +102,6 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :unique_ips_limit_enabled - validates :koding_url, - presence: true, - if: :koding_enabled - validates :plantuml_url, presence: true, if: :plantuml_enabled @@ -252,8 +250,6 @@ class ApplicationSetting < ActiveRecord::Base housekeeping_gc_period: 200, housekeeping_incremental_repack_period: 10, import_sources: Settings.gitlab['import_sources'], - koding_enabled: false, - koding_url: nil, max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], mirror_available: true, diff --git a/app/models/blob.rb b/app/models/blob.rb index acc64ffca67..31a839274b5 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -162,7 +162,7 @@ class Blob < SimpleDelegator if stored_externally? if rich_viewer rich_viewer.binary? - elsif Linguist::Language.find_by_extension(name).any? + elsif known_extension? false elsif _mime_type _mime_type.binary? diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index 7be6a14f585..e43a0fd1786 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -19,7 +19,7 @@ module Clusters def set_initial_status return unless not_installable? - if cluster&.application_ingress_installed? && cluster.application_ingress.external_ip + if cluster&.application_ingress_available? && cluster.application_ingress.external_ip self.status = 'installable' end end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index d7011ef447a..20d53b8e620 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -43,8 +43,9 @@ module Clusters delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true - delegate :installed?, to: :application_helm, prefix: true, allow_nil: true - delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true + delegate :available?, to: :application_helm, prefix: true, allow_nil: true + delegate :available?, to: :application_ingress, prefix: true, allow_nil: true + delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true enum platform_type: { kubernetes: 1 diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb index e3deedfb036..683b45331f6 100644 --- a/app/models/clusters/concerns/application_core.rb +++ b/app/models/clusters/concerns/application_core.rb @@ -15,7 +15,7 @@ module Clusters def set_initial_status return unless not_installable? - self.status = 'installable' if cluster&.application_helm_installed? + self.status = 'installable' if cluster&.application_helm_available? end def self.application_name diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index a9df59fc059..93bdf9c223d 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -66,6 +66,10 @@ module Clusters end end end + + def available? + installed? || updated? + end end end end diff --git a/app/models/concerns/blob_like.rb b/app/models/concerns/blob_like.rb index e96fefe81c4..f20f01486a5 100644 --- a/app/models/concerns/blob_like.rb +++ b/app/models/concerns/blob_like.rb @@ -2,7 +2,7 @@ module BlobLike extend ActiveSupport::Concern - include Linguist::BlobHelper + include Gitlab::BlobHelper def id raise NotImplementedError diff --git a/app/models/project.rb b/app/models/project.rb index c7ca322853f..b80e41e4a96 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -49,8 +49,11 @@ class Project < ActiveRecord::Base attachments: 2 }.freeze - # Valids ports to import from - VALID_IMPORT_PORTS = [22, 80, 443].freeze + VALID_IMPORT_PORTS = [80, 443].freeze + VALID_IMPORT_PROTOCOLS = %w(http https git).freeze + + VALID_MIRROR_PORTS = [22, 80, 443].freeze + VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze cache_markdown_field :description, pipeline: :description @@ -305,10 +308,10 @@ class Project < ActiveRecord::Base validates :namespace, presence: true validates :name, uniqueness: { scope: :namespace_id } - validates :import_url, url: { protocols: %w(http https ssh git), + validates :import_url, url: { protocols: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS }, + ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS }, allow_localhost: false, - enforce_user: true, - ports: VALID_IMPORT_PORTS }, if: [:external_import?, :import_url_changed?] + enforce_user: true }, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? } diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index 2545df06f6b..76624263aab 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -1,53 +1,5 @@ # frozen_string_literal: true -require "flowdock-git-hook" - -# Flow dock depends on Grit to compute the number of commits between two given -# commits. To make this depend on Gitaly, a monkey patch is applied -module Flowdock - class Git - # pass down a Repository all the way down - def repo - @options[:repo] - end - - def config - {} - end - - def messages - Git::Builder.new(repo: repo, - ref: @ref, - before: @from, - after: @to, - commit_url: @commit_url, - branch_url: @branch_url, - diff_url: @diff_url, - repo_url: @repo_url, - repo_name: @repo_name, - permanent_refs: @permanent_refs, - tags: tags - ).to_hashes - end - - class Builder - def commits - @repo.commits_between(@before, @after).map do |commit| - { - url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil, - id: commit.sha, - message: commit.message, - author: { - name: commit.author_name, - email: commit.author_email - } - } - end - end - end - end -end - class FlowdockService < Service prop_accessor :token validates :token, presence: true, if: :activated? diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index ba7fcb0cf93..5a38f48c542 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -14,6 +14,9 @@ class JiraService < IssueTrackerService format: { with: Gitlab::Regex.jira_transition_id_regex, message: "transition ids can have only numbers which can be split with , or ;" }, allow_blank: true + # JIRA cloud version is deprecating authentication via username and password. + # We should use username/password for JIRA server and email/api_token for JIRA cloud, + # for more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/49936. prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description before_update :reset_password @@ -95,8 +98,8 @@ class JiraService < IssueTrackerService [ { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true }, { type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' }, - { type: 'text', name: 'username', placeholder: '', required: true }, - { type: 'password', name: 'password', placeholder: '', required: true }, + { type: 'text', name: 'username', title: 'Username or Email', placeholder: 'Use a username for server version and an email for cloud version', required: true }, + { type: 'password', name: 'password', title: 'Password or API token', placeholder: 'Use a password for server version and an API token for cloud version', required: true }, { type: 'text', name: 'jira_issue_transition_id', title: 'Transition ID(s)', placeholder: 'Use , or ; to separate multiple transition IDs' } ] end diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 509e5b6089b..211e5c3fcbf 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -26,7 +26,7 @@ class PrometheusService < MonitoringService end def editable? - manual_configuration? || !prometheus_installed? + manual_configuration? || !prometheus_available? end def title @@ -56,7 +56,6 @@ class PrometheusService < MonitoringService name: 'api_url', title: 'API URL', placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'), - help: s_('PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.'), required: true } ] @@ -75,17 +74,17 @@ class PrometheusService < MonitoringService RestClient::Resource.new(api_url) if api_url && manual_configuration? && active? end - def prometheus_installed? + def prometheus_available? return false if template? return false unless project - project.clusters.enabled.any? { |cluster| cluster.application_prometheus&.installed? } + project.clusters.enabled.any? { |cluster| cluster.application_prometheus_available? } end private def synchronize_service_state - self.active = prometheus_installed? || manual_configuration? + self.active = prometheus_available? || manual_configuration? true end diff --git a/app/models/repository.rb b/app/models/repository.rb index a3a3ce179fc..37a1dd64052 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -36,7 +36,7 @@ class Repository # For example, for entry `:commit_count` there's a method called `commit_count` which # stores its data in the `commit_count` cache key. CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide - changelog license_blob license_key gitignore koding_yml + changelog license_blob license_key gitignore gitlab_ci_yml branch_names tag_names branch_count tag_count avatar exists? root_ref has_visible_content? issue_template_names merge_request_template_names xcode_project?).freeze @@ -53,7 +53,6 @@ class Repository license: %i(license_blob license_key license), contributing: :contribution_guide, gitignore: :gitignore, - koding: :koding_yml, gitlab_ci: :gitlab_ci_yml, avatar: :avatar, issue_template: :issue_template_names, @@ -488,7 +487,20 @@ class Repository end def blob_at(sha, path) - Blob.decorate(raw_repository.blob_at(sha, path), project) + blob = Blob.decorate(raw_repository.blob_at(sha, path), project) + + # Don't attempt to return a special result if there is no blob at all + return unless blob + + # Don't attempt to return a special result unless we're looking at HEAD + return blob unless head_commit&.sha == sha + + case path + when head_tree&.readme_path + ReadmeBlob.new(blob, self) + else + blob + end rescue Gitlab::Git::Repository::NoRepository nil end @@ -570,9 +582,7 @@ class Repository cache_method :merge_request_template_names, fallback: [] def readme - if readme = tree(:head)&.readme - ReadmeBlob.new(readme, self) - end + head_tree&.readme end def rendered_readme @@ -619,11 +629,6 @@ class Repository end cache_method :gitignore - def koding_yml - file_on_head(:koding) - end - cache_method :koding_yml - def gitlab_ci_yml file_on_head(:gitlab_ci) end @@ -881,10 +886,12 @@ class Repository delegate :merged_branch_names, to: :raw_repository - def merge_base(first_commit_id, second_commit_id) - first_commit_id = commit(first_commit_id).try(:id) || first_commit_id - second_commit_id = commit(second_commit_id).try(:id) || second_commit_id - raw_repository.merge_base(first_commit_id, second_commit_id) + def merge_base(*commits_or_ids) + commit_ids = commits_or_ids.map do |commit_or_id| + commit_or_id.is_a?(::Commit) ? commit_or_id.id : commit_or_id + end + + raw_repository.merge_base(*commit_ids) end def ancestor?(ancestor_id, descendant_id) diff --git a/app/models/tree.rb b/app/models/tree.rb index 3641c33254c..cd385872171 100644 --- a/app/models/tree.rb +++ b/app/models/tree.rb @@ -2,6 +2,7 @@ class Tree include Gitlab::MarkupHelper + include Gitlab::Utils::StrongMemoize attr_accessor :repository, :sha, :path, :entries @@ -16,32 +17,36 @@ class Tree @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive) end - def readme - return @readme if defined?(@readme) - - available_readmes = blobs.select do |blob| - Gitlab::FileDetector.type_of(blob.name) == :readme - end - - previewable_readmes = available_readmes.select do |blob| - previewable?(blob.name) - end - - plain_readmes = available_readmes.select do |blob| - plain?(blob.name) + def readme_path + strong_memoize(:readme_path) do + available_readmes = blobs.select do |blob| + Gitlab::FileDetector.type_of(blob.name) == :readme + end + + previewable_readmes = available_readmes.select do |blob| + previewable?(blob.name) + end + + plain_readmes = available_readmes.select do |blob| + plain?(blob.name) + end + + # Prioritize previewable over plain readmes + entry = previewable_readmes.first || plain_readmes.first + next nil unless entry + + if path == '/' + entry.name + else + File.join(path, entry.name) + end end + end - # Prioritize previewable over plain readmes - readme_tree = previewable_readmes.first || plain_readmes.first - - # Return if we can't preview any of them - if readme_tree.nil? - return @readme = nil + def readme + strong_memoize(:readme) do + repository.blob_at(sha, readme_path) if readme_path end - - readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name) - - @readme = repository.blob_at(sha, readme_path) end def trees diff --git a/app/models/user.rb b/app/models/user.rb index a0665518cf5..34efb22b359 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -264,7 +264,7 @@ class User < ActiveRecord::Base scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } scope :confirmed, -> { where.not(confirmed_at: nil) } - scope :by_username, -> (usernames) { iwhere(username: usernames) } + scope :by_username, -> (usernames) { iwhere(username: Array(usernames).map(&:to_s)) } scope :for_todos, -> (todos) { where(id: todos.select(:user_id)) } # Limits the users to those that have TODOs, optionally in the given state. diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index 2c0e8659fc1..1cd05cf3aac 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -6,8 +6,7 @@ class UserCallout < ActiveRecord::Base enum feature_name: { gke_cluster_integration: 1, gcp_signup_offer: 2, - cluster_security_warning: 3, - gold_trial: 4 + cluster_security_warning: 3 } validates :user, presence: true diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 42fd213d03b..c5e349ae913 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -160,7 +160,9 @@ class WikiPage # Returns boolean True or False if this instance # is an old version of the page. def historical? - @page.historical? && last_version.sha != version.sha + return false unless last_commit_sha && version + + @page.historical? && last_commit_sha != version.sha end # Returns boolean True or False if this instance diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index d2434d96fd7..79cd3606aec 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -36,8 +36,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated contribution_guide_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), kubernetes_cluster_anchor_data, - gitlab_ci_anchor_data, - koding_anchor_data + gitlab_ci_anchor_data ].compact.reject { |item| item.enabled } end @@ -125,43 +124,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated add_special_file_path(file_name: 'README.md') end - def add_koding_stack_path - project_new_blob_path( - project, - default_branch || 'master', - file_name: '.koding.yml', - commit_message: "Add Koding stack script", - content: <<-CONTENT.strip_heredoc - provider: - aws: - access_key: '${var.aws_access_key}' - secret_key: '${var.aws_secret_key}' - resource: - aws_instance: - #{project.path}-vm: - instance_type: t2.nano - user_data: |- - - # Created by GitLab UI for :> - - echo _KD_NOTIFY_@Installing Base packages...@ - - apt-get update -y - apt-get install git -y - - echo _KD_NOTIFY_@Cloning #{project.name}...@ - - export KODING_USER=${var.koding_user_username} - export REPO_URL=#{root_url}${var.koding_queryString_repo}.git - export BRANCH=${var.koding_queryString_branch} - - sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH - - echo _KD_NOTIFY_@#{project.name} cloned.@ - CONTENT - ) - end - def license_short_name license = repository.license license&.nickname || license&.name || 'LICENSE' @@ -310,14 +272,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end end - def koding_anchor_data - if current_user && can_current_user_push_code? && koding_enabled? && repository.koding_yml.blank? - AnchorData.new(false, - _('Set up Koding'), - add_koding_stack_path) - end - end - def tags_to_show project.tag_list.take(MAX_TAGS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord end @@ -363,8 +317,4 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated branch_name: branch_name ) end - - def koding_enabled? - Gitlab::CurrentSettings.koding_enabled? - end end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 7bdcfcc38f7..066a5b1885c 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -9,7 +9,7 @@ class BuildDetailsEntity < JobEntity expose :runner, using: RunnerEntity expose :pipeline, using: PipelineEntity - expose :deployment_status, if: -> (*) { build.has_environment? } do + expose :deployment_status, if: -> (*) { build.starts_environment? } do expose :deployment_status, as: :status expose :persisted_environment, as: :environment, with: EnvironmentEntity diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb index 16a477c92fa..c3f7d4651fb 100644 --- a/app/serializers/issue_entity.rb +++ b/app/serializers/issue_entity.rb @@ -42,6 +42,6 @@ class IssueEntity < IssuableEntity end expose :preview_note_path do |issue| - preview_markdown_path(issue.project, quick_actions_target_type: 'Issue', quick_actions_target_id: issue.id) + preview_markdown_path(issue.project, quick_actions_target_type: 'Issue', quick_actions_target_id: issue.iid) end end diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 380e8804f51..9ec24f799ef 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -222,7 +222,7 @@ class MergeRequestWidgetEntity < IssuableEntity end expose :preview_note_path do |merge_request| - preview_markdown_path(merge_request.project, quick_actions_target_type: 'MergeRequest', quick_actions_target_id: merge_request.id) + preview_markdown_path(merge_request.project, quick_actions_target_type: 'MergeRequest', quick_actions_target_id: merge_request.iid) end expose :merge_commit_path do |merge_request| diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb index 3d88c4f064e..b6c30da4d3a 100644 --- a/app/services/applications/create_service.rb +++ b/app/services/applications/create_service.rb @@ -9,6 +9,7 @@ module Applications end # rubocop: enable CodeReuse/ActiveRecord + # EE would override and use `request` arg def execute(request) Doorkeeper::Application.create(@params) end diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb new file mode 100644 index 00000000000..55f917798de --- /dev/null +++ b/app/services/clusters/applications/create_service.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Clusters + module Applications + class CreateService + InvalidApplicationError = Class.new(StandardError) + + attr_reader :cluster, :current_user, :params + + def initialize(cluster, user, params = {}) + @cluster = cluster + @current_user = user + @params = params.dup + end + + def execute(request) + create_application.tap do |application| + if application.has_attribute?(:hostname) + application.hostname = params[:hostname] + end + + if application.respond_to?(:oauth_application) + application.oauth_application = create_oauth_application(application, request) + end + + application.save! + + Clusters::Applications::ScheduleInstallationService.new(application).execute + end + end + + private + + def create_application + builder.call(@cluster) + end + + def builder + builders[application_name] || raise(InvalidApplicationError, "invalid application: #{application_name}") + end + + def builders + { + "helm" => -> (cluster) { cluster.application_helm || cluster.build_application_helm }, + "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress }, + "prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus }, + "runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner }, + "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter } + } + end + + def application_name + params[:application] + end + + def create_oauth_application(application, request) + oauth_application_params = { + name: params[:application], + redirect_uri: application.callback_url, + scopes: 'api read_user openid', + owner: current_user + } + + ::Applications::CreateService.new(current_user, oauth_application_params).execute(request) + end + end + end +end diff --git a/app/services/clusters/applications/schedule_installation_service.rb b/app/services/clusters/applications/schedule_installation_service.rb index 4ead4f619c8..d75ba70c27e 100644 --- a/app/services/clusters/applications/schedule_installation_service.rb +++ b/app/services/clusters/applications/schedule_installation_service.rb @@ -2,8 +2,14 @@ module Clusters module Applications - class ScheduleInstallationService < ::BaseService - def execute(application) + class ScheduleInstallationService + attr_reader :application + + def initialize(application) + @application = application + end + + def execute application.make_scheduled! ClusterInstallAppWorker.perform_async(application.name, application.id) diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb index e3e0cfa462c..c6e955800af 100644 --- a/app/services/clusters/create_service.rb +++ b/app/services/clusters/create_service.rb @@ -1,36 +1,34 @@ # frozen_string_literal: true module Clusters - class CreateService < BaseService - attr_reader :access_token + class CreateService + attr_reader :current_user, :params - def execute(access_token = nil) - @access_token = access_token + def initialize(user = nil, params = {}) + @current_user, @params = user, params.dup + end - raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster? + def execute(project:, access_token: nil) + raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?(project) - create_cluster.tap do |cluster| + cluster_params = params.merge(user: current_user, projects: [project]) + cluster_params[:provider_gcp_attributes].try do |provider| + provider[:access_token] = access_token + end + + create_cluster(cluster_params).tap do |cluster| ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? end end private - def create_cluster + def create_cluster(cluster_params) Clusters::Cluster.create(cluster_params) end - def cluster_params - return @cluster_params if defined?(@cluster_params) - - params[:provider_gcp_attributes].try do |provider| - provider[:access_token] = access_token - end - - @cluster_params = params.merge(user: current_user, projects: [project]) - end - - def can_create_cluster? + # EE would override this method + def can_create_cluster?(project) project.clusters.empty? end end diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb index 98fdeec4fb1..25d26e761b1 100644 --- a/app/services/clusters/update_service.rb +++ b/app/services/clusters/update_service.rb @@ -1,7 +1,13 @@ # frozen_string_literal: true module Clusters - class UpdateService < BaseService + class UpdateService + attr_reader :current_user, :params + + def initialize(user = nil, params = {}) + @current_user, @params = user, params.dup + end + def execute(cluster) cluster.update(params) end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index fe47aa2f140..0bf0e967dcc 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -14,9 +14,11 @@ module Groups group.assign_attributes(params) begin - after_update if group.save + success = group.save - true + after_update if success + + success rescue Gitlab::UpdatePathError => e group.errors.add(:base, e.message) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 575678da1fa..729bc991294 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -211,7 +211,7 @@ module SystemNoteService # "closed via bc17db76" # # Returns the created Note object - def change_status(noteable, project, author, status, source) + def change_status(noteable, project, author, status, source = nil) body = status.dup body << " via #{source.gfm_reference(project)}" if source diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb index 34724e0250d..144d738dfd2 100644 --- a/app/services/web_hook_service.rb +++ b/app/services/web_hook_service.rb @@ -22,7 +22,7 @@ class WebHookService end def execute - start_time = Time.now + start_time = Gitlab::Metrics::System.monotonic_time response = if parsed_url.userinfo.blank? make_request(hook.url) @@ -35,7 +35,7 @@ class WebHookService url: hook.url, request_data: data, response: response, - execution_duration: Time.now - start_time + execution_duration: Gitlab::Metrics::System.monotonic_time - start_time ) { @@ -49,7 +49,7 @@ class WebHookService url: hook.url, request_data: data, response: InternalErrorResponse.new, - execution_duration: Time.now - start_time, + execution_duration: Gitlab::Metrics::System.monotonic_time - start_time, error_message: e.to_s ) diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml deleted file mode 100644 index 8b635b08abd..00000000000 --- a/app/views/admin/application_settings/_koding.html.haml +++ /dev/null @@ -1,22 +0,0 @@ -= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-koding-settings'), html: { class: 'fieldset-form' } do |f| - = form_errors(@application_setting) - - %fieldset - .form-group - .form-check - = f.check_box :koding_enabled, class: 'form-check-input' - = f.label :koding_enabled, class: 'form-check-label' do - Enable Koding - .form-text.text-muted - Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again. - .form-group - = f.label :koding_url, 'Koding URL', class: 'label-bold' - = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090' - .form-text.text-muted - Koding has integration enabled out of the box for the - %strong gitlab - team, and you need to provide that team's URL here. Learn more in the - = succeed "." do - = link_to "Koding administration documentation", help_page_path("administration/integration/koding") - - = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index 279db189a24..65e4723afe6 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -68,18 +68,6 @@ .settings-content = render 'terms' -- if koding_enabled? - %section.settings.as-koding.no-animate#js-koding-settings{ class: ('expanded' if expanded_by_default?) } - .settings-header - %h4 - = _('Koding') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded_by_default? ? _('Collapse') : _('Expand') - %p - = _('Online IDE integration settings.') - .settings-content - = render 'koding' - = render_if_exists 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default? %section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded_by_default?) } diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index 2a117c1414e..5e05568e384 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -10,11 +10,11 @@ .col-sm-10 = render 'shared/choose_group_avatar_button', f: f - = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group + = render 'shared/old_visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false .form-group.row .offset-sm-2.col-sm-10 - = render 'shared/allow_request_access', form: f + = render 'shared/allow_request_access', form: f, bold_label: true = render 'groups/group_admin_settings', f: f diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 0c683f86252..21c1260e982 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -119,7 +119,7 @@ = _("<strong>%{group_name}</strong> group members").html_safe % { group_name: @group.name } %span.badge.badge-pill= @group.members.size .float-right - = link_to icon('pencil-square-o', text: _('Manage access')), polymorphic_url([@group, :members]), class: "btn btn-sm" + = link_to icon('pencil-square-o', text: _('Manage access')), group_group_members_path(@group), class: "btn btn-sm" %ul.content-list.group-users-list.content-list.members-list = render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false } .card-footer diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index fefb4c7455d..03cce4745aa 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -183,7 +183,7 @@ project members %span.badge.badge-pill= @project.users.size .float-right - = link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-sm" + = link_to icon('pencil-square-o', text: 'Manage access'), project_project_members_path(@project), class: "btn btn-sm" %ul.content-list.project_members.members-list = render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false } .card-footer diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml index 3cee5841bbc..31d4b3da4f1 100644 --- a/app/views/dashboard/activity.html.haml +++ b/app/views/dashboard/activity.html.haml @@ -4,9 +4,6 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - - page_title "Activity" - header_title "Activity", activity_dashboard_path diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 985928305a2..50f39f93283 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -3,9 +3,6 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - - if params[:filter].blank? && @groups.empty? = render 'shared/groups/empty_state' - else diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml index 91f58ddcfcc..86a21e24ac9 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -4,9 +4,6 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues") -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - .top-area = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set .nav-controls diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 27f53a8d1c6..61aae31be60 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -2,9 +2,6 @@ - page_title _("Merge Requests") - @breadcrumb_link = merge_requests_dashboard_path(assignee_id: current_user.id) -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - .top-area = render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set .nav-controls diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index f0d16936a51..deed774a4a5 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -4,9 +4,6 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - - page_title "Projects" - header_title "Projects", dashboard_projects_path diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 42638b8528d..8933d9e31ff 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -4,9 +4,6 @@ - page_title "Starred Projects" - header_title "Projects", dashboard_projects_path -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - %div{ class: container_class } = render "projects/last_push" = render 'dashboard/projects_head' diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index bbfa4cc7413..8b3974d97f8 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -2,9 +2,6 @@ - page_title "Todos" - header_title "Todos", dashboard_todos_path -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - - if current_user.todos.any? .top-area %ul.nav-links.mobile-separator.nav.nav-tabs diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 9a827523ed4..004a3528d4b 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -24,7 +24,7 @@ %p.gl-field-hint.text-secondary Minimum length is #{@minimum_password_length} characters - if Gitlab::CurrentSettings.current_application_settings.enforce_terms? .form-group - = check_box_tag :terms_opt_in, '1', false, required: true + = check_box_tag :terms_opt_in, '1', false, required: true, class: 'qa-new-user-accept-terms' = label_tag :terms_opt_in do - terms_link = link_to s_("I accept the|Terms of Service and Privacy Policy"), terms_path, target: "_blank" - accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link } diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 1d8b9c5bc8f..a3eafc61d0a 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -2,9 +2,6 @@ - page_title _("Groups") - header_title _("Groups"), dashboard_groups_path -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - - if current_user = render 'dashboard/groups_head' - else @@ -13,7 +10,7 @@ - if cookies[:explore_groups_landing_dismissed] != 'true' .explore-groups.landing.content-block.js-explore-groups-landing.hide - %button.dismiss-button{ type: 'button', 'aria-label' => _('Dismiss') }= icon('times') + %button.dismiss-button{ type: 'button', 'aria-label' => _('Dismiss') }= sprite_icon('close', size: 16) .svg-container = custom_icon('icon_explore_groups_splash') .inner-content diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index 16be5791f83..452f390695c 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -2,9 +2,6 @@ - page_title _("Projects") - header_title _("Projects"), dashboard_projects_path -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - - if current_user = render 'dashboard/projects_head' - else diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index 16be5791f83..452f390695c 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -2,9 +2,6 @@ - page_title _("Projects") - header_title _("Projects"), dashboard_projects_path -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - - if current_user = render 'dashboard/projects_head' - else diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index 16be5791f83..452f390695c 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -2,9 +2,6 @@ - page_title _("Projects") - header_title _("Projects"), dashboard_projects_path -= content_for :above_breadcrumbs_content do - = render_if_exists "shared/gold_trial_callout" - - if current_user = render 'dashboard/projects_head' - else diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index fc17dd2d310..f3792c5e397 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -3,31 +3,31 @@ - expanded = Rails.env.test? -%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded' if expanded) } +%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') } .settings-header - %h4 - = _('General') + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' } + = _('Naming, visibility') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') + = _('Collapse') %p - = _('Update your group name, description, avatar, and other general settings.') + = _('Update your group name, description, avatar, and visibility.') .settings-content = render 'groups/settings/general' %section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded) } .settings-header - %h4 - = _('Permissions') + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' } + = _('Permissions, LFS, 2FA') %button.btn.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p - = _('Enable or disable certain group features and choose access levels.') + = _('Advanced permissions, Large File Storage and Two-Factor authentication settings.') .settings-content = render 'groups/settings/permissions' -%section.settings.no-animate{ class: ('expanded' if expanded) } +%section.settings.no-animate#js-badge-settings{ class: ('expanded' if expanded) } .settings-header - %h4 + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' } = s_('GroupSettings|Badges') %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' @@ -39,8 +39,8 @@ %section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) } .settings-header - %h4 - = _('Advanced') + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' } + = _('Path, transfer, remove') %button.btn.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 683129fdf6e..86c5f6a7aa3 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -27,7 +27,7 @@ .col-sm-10 = render 'shared/choose_group_avatar_button', f: f - = render 'shared/visibility_level', f: f, visibility_level: default_group_visibility, can_change_visibility_level: true, form_model: @group + = render 'shared/old_visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false = render 'create_chat_team', f: f if Gitlab.config.mattermost.enabled diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml index 3814d45929d..5d211d0e186 100644 --- a/app/views/groups/settings/_advanced.html.haml +++ b/app/views/groups/settings/_advanced.html.haml @@ -23,16 +23,6 @@ = f.submit 'Change group path', class: 'btn btn-warning' -.sub-section - %h4.danger-title Remove group - = form_tag(@group, method: :delete) do - %p - Removing group will cause all child projects and resources to be removed. - %br - %strong Removed group can not be restored! - - = button_to 'Remove group', '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(@group) } - - if supports_nested_groups? .sub-section %h4.warning-title Transfer group @@ -47,3 +37,13 @@ %li You will need to update your local repositories to point to the new location. %li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility. = f.submit 'Transfer group', class: 'btn btn-warning' + +.sub-section + %h4.danger-title= _('Remove group') + = form_tag(@group, method: :delete) do + %p + = _('Removing group will cause all child projects and resources to be removed.') + %br + %strong= _('Removed group can not be restored!') + + = button_to _('Remove group'), '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(@group) } diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml index 0e225fe33a5..0424ece037d 100644 --- a/app/views/groups/settings/_general.html.haml +++ b/app/views/groups/settings/_general.html.haml @@ -1,39 +1,33 @@ -= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f| += form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-settings-form' }, authenticity_token: true do |f| %input{ type: 'hidden', name: 'update_section', value: 'js-general-settings' } = form_errors(@group) %fieldset .row - .form-group.col-md-9 - = f.label :name, class: 'label-bold' do - Group name + .form-group.col-md-5 + = f.label :name, _('Group name'), class: 'label-bold' = f.text_field :name, class: 'form-control' - .form-group.col-md-3 - = f.label :id, class: 'label-bold' do - Group ID - = f.text_field :id, class: 'form-control', readonly: true + .form-group.col-md-7 + = f.label :id, _('Group ID'), class: 'label-bold' + = f.text_field :id, class: 'form-control w-auto', readonly: true - .form-group - = f.label :description, class: 'label-bold' do - Group description - %span.light (optional) - = f.text_area :description, class: 'form-control', rows: 3, maxlength: 250 + .row.prepend-top-8 + .form-group.col-md-9.append-bottom-0 + = f.label :description, _('Group description (optional)'), class: 'label-bold' + = f.text_area :description, class: 'form-control', rows: 3, maxlength: 250 = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group - .form-group.row - .col-sm-12 - .avatar-container.s160 - = group_icon(@group, alt: '', class: 'avatar group-avatar s160') - %p.light - - if @group.avatar? - You can change the group avatar here - - else - You can upload a group avatar here - = render 'shared/choose_group_avatar_button', f: f - - if @group.avatar? - %hr - = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-danger btn-inverted' + .form-group.prepend-top-default.append-bottom-20 + .avatar-container.s90 + = group_icon(@group, alt: '', class: 'avatar group-avatar s90') + = f.label :avatar, _('Group avatar'), class: 'label-bold d-block' + = render 'shared/choose_group_avatar_button', f: f + - if @group.avatar? + %hr + = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-danger btn-inverted' - = f.submit 'Save group', class: 'btn btn-success' + = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group + + = f.submit _('Save changes'), class: 'btn btn-success mt-4 js-dirty-submit' diff --git a/app/views/groups/settings/_lfs.html.haml b/app/views/groups/settings/_lfs.html.haml new file mode 100644 index 00000000000..4674d561c12 --- /dev/null +++ b/app/views/groups/settings/_lfs.html.haml @@ -0,0 +1,15 @@ +- docs_link_url = help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') +- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_link_url } + +%h5= _('Large File Storage') + +%p= s_('Check the %{docs_link_start}documentation%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe } + +.form-group.append-bottom-default + .form-check + = f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'form-check-input' + = f.label :lfs_enabled, class: 'form-check-label' do + %span + = _('Allow projects within this group to use Git LFS') + %br/ + %span.text-muted= _('This setting can be overridden in each project.') diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index 8dc88ec446c..6b0a6e7ed99 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -1,29 +1,24 @@ -= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f| += form_for @group, html: { multipart: true, class: 'gl-show-field-errors js-general-permissions-form' }, authenticity_token: true do |f| %input{ type: 'hidden', name: 'update_section', value: 'js-permissions-settings' } = form_errors(@group) %fieldset - = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group + %h5= _('Permissions') + .form-group + = render 'shared/allow_request_access', form: f - .form-group.row - .offset-sm-2.col-sm-10 - = render 'shared/allow_request_access', form: f - - .form-group.row - %label.col-form-label.col-sm-2.pt-0 - = s_('GroupSettings|Share with group lock') - .col-sm-10 - .form-check - = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input' - = f.label :share_with_group_lock, class: 'form-check-label' do - %strong - - group_link = link_to @group.name, group_path(@group) - = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } - %br - %span.descr= share_with_group_lock_help_text(@group) - - = render 'groups/group_admin_settings', f: f + .form-group.append-bottom-default + .form-check + = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input' + = f.label :share_with_group_lock, class: 'form-check-label' do + %span + - group_link = link_to @group.name, group_path(@group) + = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } + %br + %span.descr.text-muted= share_with_group_lock_help_text(@group) + = render 'groups/settings/lfs', f: f + = render 'groups/settings/two_factor_auth', f: f = render_if_exists 'groups/member_lock_setting', f: f, group: @group - = f.submit 'Save group', class: 'btn btn-success' + = f.submit _('Save changes'), class: 'btn btn-success prepend-top-default js-dirty-submit' diff --git a/app/views/groups/settings/_two_factor_auth.html.haml b/app/views/groups/settings/_two_factor_auth.html.haml new file mode 100644 index 00000000000..5d3f1cbb279 --- /dev/null +++ b/app/views/groups/settings/_two_factor_auth.html.haml @@ -0,0 +1,16 @@ +- docs_link_url = help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group') +- docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_link_url } + +%h5= _('Two-factor authentication') + +%p= s_('Check the %{docs_link_start}documentation%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe } + +.form-group + .form-check + = f.check_box :require_two_factor_authentication, class: 'form-check-input' + = f.label :require_two_factor_authentication, class: 'form-check-label' do + %span= _('Require all users in this group to setup Two-factor authentication') +.form-group + = f.label :two_factor_grace_period, _('Time before enforced'), class: 'label-bold' + = f.text_field :two_factor_grace_period, class: 'form-control form-control-sm w-auto' + .form-text.text-muted= _('Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication') diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml deleted file mode 100644 index bb7f9ba7ae4..00000000000 --- a/app/views/koding/index.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.row-content-block.second-block.center - %p - = icon('circle', class: 'cgreen') - Integration is active for - = link_to koding_project_url, target: '_blank', rel: 'noopener noreferrer' do - #{Gitlab::CurrentSettings.koding_url} diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml index 261d758622b..648124d3870 100644 --- a/app/views/layouts/header/_current_user_dropdown.html.haml +++ b/app/views/layouts/header/_current_user_dropdown.html.haml @@ -19,12 +19,7 @@ - if current_user_menu?(:settings) %li = link_to s_("CurrentUser|Settings"), profile_path - - if current_user_menu?(:help) - %li - = link_to _("Help"), help_path - - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile) - %li.divider - = render 'shared/user_dropdown_contributing_link' - if current_user_menu?(:sign_out) + %li.divider %li = link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 39604611440..596fc3985b3 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -53,6 +53,12 @@ = sprite_icon('todo-done', size: 16) %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) } = todos_count_format(todos_pending_count) + %li.nav-item.header-help.dropdown + = link_to help_path, class: 'header-help-dropdown-toggle', data: { toggle: "dropdown" } do + = sprite_icon('question', size: 16) + = sprite_icon('angle-down', css_class: 'caret-down') + .dropdown-menu.dropdown-menu-right + = render 'layouts/header/help_dropdown' - if header_link?(:user_dropdown) %li.nav-item.header-user.dropdown = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml new file mode 100644 index 00000000000..953c0e7f46c --- /dev/null +++ b/app/views/layouts/header/_help_dropdown.html.haml @@ -0,0 +1,6 @@ +%ul + - if current_user_menu?(:help) + %li + = link_to _("Help"), help_path + - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile) + = render 'shared/user_dropdown_contributing_link' diff --git a/app/views/layouts/koding.html.haml b/app/views/layouts/koding.html.haml deleted file mode 100644 index 45ccd38f687..00000000000 --- a/app/views/layouts/koding.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- page_title _("Koding") -- page_description _("Koding Dashboard") -- header_title _("Koding"), koding_path - -= render template: "layouts/application" diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml index c35451827c8..f53bd2b5e4d 100644 --- a/app/views/layouts/nav/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -1,7 +1,6 @@ - container = @no_breadcrumb_container ? 'container-fluid' : container_class - hide_top_links = @hide_top_links || false -= yield :above_breadcrumbs_content %nav.breadcrumbs{ role: "navigation", class: [container, @content_class] } .breadcrumbs-container - if defined?(@left_sidebar) diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 3625224fbcd..174033f3d49 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -86,7 +86,7 @@ - if project_nav_tab? :issues = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do - = link_to project_issues_path(@project), class: 'shortcuts-issues' do + = link_to project_issues_path(@project), class: 'shortcuts-issues qa-issues-item' do .nav-icon-container = sprite_icon('issues') %span.nav-item-name @@ -115,7 +115,7 @@ = boards_link_text = nav_link(controller: :labels) do - = link_to project_labels_path(@project), title: _('Labels') do + = link_to project_labels_path(@project), title: _('Labels'), class: 'qa-labels-link' do %span = _('Labels') diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index ced6a2a0399..61ed951dea9 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -76,8 +76,6 @@ = render 'projects/buttons/download', project: @project, ref: @ref .d-none.d-sm-inline-flex = render 'projects/buttons/dropdown' - .d-none.d-sm-inline-flex - = render 'projects/buttons/koding' .d-none.d-sm-inline-flex = render 'shared/notifications/button', notification_setting: @notification_setting diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml index 5adca007f7e..45e1d32980c 100644 --- a/app/views/projects/_wiki.html.haml +++ b/app/views/projects/_wiki.html.haml @@ -5,14 +5,14 @@ = render_wiki_content(@wiki_home, legacy_render_context(params)) - else - can_create_wiki = can?(current_user, :create_wiki, @project) - .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] } - .text-center{ class: container_class } + .landing{ class: [('row-content-block row p-0 align-items-center' if can_create_wiki), ('content-block' unless can_create_wiki)] } + .col-12.col-md-3.p-0 + .svg-content + = image_tag 'illustrations/wiki_login_empty.svg' + .col-12.col-md-9.text-center.text-md-left.pl-md-0.pl-sm-3.mb-4 %h4 - This project does not have a wiki homepage yet + = _("This project does not have a wiki homepage yet") - if can_create_wiki %p - Add a homepage to your wiki that contains information about your project - %p - We recommend you - = link_to "add a homepage", project_wiki_path(@project, :home) - to your project's wiki and GitLab will show it here instead of this message. + = _("Add a homepage to your wiki that contains information about your project and GitLab will display it here instead of this message.") + = link_to _("Create your first page"), project_wiki_path(@project, :home) + '?view=create', class: "btn btn-primary" diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 87b165e581a..09295940529 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,7 +1,7 @@ - breadcrumb_title _('Artifacts') - page_title @path.presence, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' -= render "projects/jobs/header", show_controls: false += render "projects/jobs/header" - add_to_breadcrumbs(s_('CICD|Jobs'), project_jobs_path(@project)) - add_to_breadcrumbs("##{@build.id}", project_jobs_path(@project)) diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml index f7174d6b2c6..808b4acc8f3 100644 --- a/app/views/projects/artifacts/file.html.haml +++ b/app/views/projects/artifacts/file.html.haml @@ -1,6 +1,6 @@ - page_title @path, 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' -= render "projects/jobs/header", show_controls: false += render "projects/jobs/header" .tree-holder .nav-block diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml deleted file mode 100644 index e665ca61da8..00000000000 --- a/app/views/projects/buttons/_koding.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -- if koding_enabled? && current_user && @repository.koding_yml && @project.can_current_user_push_code? - = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank', rel: 'noopener noreferrer' do - _('Run in IDE (Koding)') diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml index 141314b4e4e..73cfea0ef92 100644 --- a/app/views/projects/clusters/_banner.html.haml +++ b/app/views/projects/clusters/_banner.html.haml @@ -9,7 +9,7 @@ = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details") - if show_cluster_security_warning? - .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning{ data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } } - %button.close.js-close{ type: "button" } × + .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning + %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } } × = s_("ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application.") = link_to s_("More information"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications') diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml index 85d1002243b..73b11d509d3 100644 --- a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml +++ b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml @@ -1,6 +1,6 @@ - link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer') -.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } - %button.close.js-close{ type: "button" } × +.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' } + %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } × .gcp-signup-offer--content .gcp-signup-offer--icon.append-right-8 = sprite_icon("information", size: 16) diff --git a/app/views/projects/deploy_tokens/_form.html.haml b/app/views/projects/deploy_tokens/_form.html.haml index 8b7535397bc..5412fcbc9d8 100644 --- a/app/views/projects/deploy_tokens/_form.html.haml +++ b/app/views/projects/deploy_tokens/_form.html.haml @@ -6,24 +6,24 @@ .form-group = f.label :name, class: 'label-bold' - = f.text_field :name, class: 'form-control', required: true + = f.text_field :name, class: 'form-control qa-deploy-token-name', required: true .form-group = f.label :expires_at, class: 'label-bold' - = f.text_field :expires_at, class: 'datepicker form-control', value: f.object.expires_at + = f.text_field :expires_at, class: 'datepicker form-control qa-deploy-token-expires-at', value: f.object.expires_at .form-group = f.label :scopes, class: 'label-bold' %fieldset.form-group.form-check - = f.check_box :read_repository, class: 'form-check-input' + = f.check_box :read_repository, class: 'form-check-input qa-deploy-token-read-repository' = label_tag ("deploy_token_read_repository"), 'read_repository', class: 'label-bold form-check-label' .text-secondary= s_('DeployTokens|Allows read-only access to the repository') - if container_registry_enabled?(project) %fieldset.form-group.form-check - = f.check_box :read_registry, class: 'form-check-input' + = f.check_box :read_registry, class: 'form-check-input qa-deploy-token-read-registry' = label_tag ("deploy_token_read_registry"), 'read_registry', class: 'label-bold form-check-label' .text-secondary= s_('DeployTokens|Allows read-only access to the registry images') .prepend-top-default - = f.submit s_('DeployTokens|Create deploy token'), class: 'btn btn-success' + = f.submit s_('DeployTokens|Create deploy token'), class: 'btn btn-success qa-create-deploy-token' diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml index 33faab0c510..4619522cfaf 100644 --- a/app/views/projects/deploy_tokens/_index.html.haml +++ b/app/views/projects/deploy_tokens/_index.html.haml @@ -1,6 +1,6 @@ - expanded = expand_deploy_tokens_section?(@new_deploy_token) -%section.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded) } +%section.qa-deploy-tokens-settings.settings.no-animate#js-deploy-tokens{ class: ('expanded' if expanded) } .settings-header %h4= s_('DeployTokens|Deploy Tokens') %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' } diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml index 5dd9ffba074..c805ee73acc 100644 --- a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml +++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml @@ -1,18 +1,18 @@ -.created-deploy-token-container.info-well +.qa-created-deploy-token-section.created-deploy-token-container.info-well .well-segment %h5.prepend-top-0 = s_('DeployTokens|Your New Deploy Token') .form-group .input-group - = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' + = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus qa-deploy-token-user' .input-group-append = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left') %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.") .form-group .input-group - = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' + = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus qa-deploy-token' .input-group-append = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left') %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.") diff --git a/app/views/projects/jobs/_header.html.haml b/app/views/projects/jobs/_header.html.haml index e7245622b80..018ff093475 100644 --- a/app/views/projects/jobs/_header.html.haml +++ b/app/views/projects/jobs/_header.html.haml @@ -1,4 +1,3 @@ -- show_controls = local_assigns.fetch(:show_controls, true) - pipeline = @build.pipeline .content-block.build-header.top-area.page-content-header @@ -20,12 +19,3 @@ = render "projects/jobs/user" if @build.user = time_ago_with_tooltip(@build.created_at) - - - if show_controls - .nav-controls - - if can?(current_user, :create_issue, @project) && @build.failed? - = link_to "New issue", new_project_issue_path(@project, issue: build_failed_issue_options), class: 'btn btn-success btn-inverted' - - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry job", retry_project_job_path(@project, @build), class: 'btn btn-inverted-secondary', method: :post - %button.btn.btn-default.float-right.d-block.d-sm-none.d-md-none.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } - = icon('angle-double-left') diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 02a088d338b..475bae887ec 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -1,52 +1,13 @@ - @no_container = true -- add_to_breadcrumbs "Jobs", project_jobs_path(@project) +- add_to_breadcrumbs _("Jobs"), project_jobs_path(@project) - breadcrumb_title "##{@build.id}" -- page_title "#{@build.name} (##{@build.id})", "Jobs" +- page_title "#{@build.name} (##{@build.id})", _("Jobs") - content_for :page_specific_javascripts do = stylesheet_link_tag 'page_bundles/xterm' %div{ class: container_class } - .build-page.js-build-page - #js-build-header-vue - - - if @build.running? || @build.has_trace? - .build-trace-container.prepend-top-default - .top-bar.js-top-bar - .js-truncated-info.truncated-info.d-none.d-sm-block.float-left.hidden< - Showing last - %span.js-truncated-info-size.truncated-info-size>< - of log - - %a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw - - .controllers.float-right - - if @build.has_trace? - = link_to raw_project_job_path(@project, @build), - title: 'Show complete raw', - data: { placement: 'top', container: 'body' }, - class: 'js-raw-link-controller has-tooltip controllers-buttons' do - = icon('file-text-o') - - - if @build.erasable? && can?(current_user, :erase_build, @build) - = link_to erase_project_job_path(@project, @build), - method: :post, - data: { confirm: 'Are you sure you want to erase this build?', placement: 'top', container: 'body' }, - title: 'Erase job log', - class: 'has-tooltip js-erase-link controllers-buttons' do - = icon('trash') - .has-tooltip.controllers-buttons{ title: 'Scroll to top', data: { placement: 'top', container: 'body'} } - %button.js-scroll-up.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true } - = custom_icon('scroll_up') - .has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} } - %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true } - = custom_icon('scroll_down') - - = render 'shared/builds/build_output' - - #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } } - -.js-build-options{ data: javascript_build_options } - -#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json), - runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'), - runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings') } } + #js-job-vue-app{ data: { endpoint: project_job_path(@project, @build, format: :json), + runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'), + runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings'), + build_options: javascript_build_options } } diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 11a05eada30..06ee883d6dc 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -8,7 +8,7 @@ - if can_admin_label - content_for(:header_content) do .nav-controls - = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success" + = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new" - if labels_or_filters #promote-label-modal diff --git a/app/views/projects/services/prometheus/_configuration_banner.html.haml b/app/views/projects/services/prometheus/_configuration_banner.html.haml index 898b55e4b39..dfcb1c5d240 100644 --- a/app/views/projects/services/prometheus/_configuration_banner.html.haml +++ b/app/views/projects/services/prometheus/_configuration_banner.html.haml @@ -7,7 +7,7 @@ - else .container-fluid .row - - if service.prometheus_installed? + - if service.prometheus_available? .col-sm-2 .svg-container = image_tag 'illustrations/monitoring/getting_started.svg' diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index aba289c790f..283031b06da 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -8,8 +8,8 @@ = render partial: 'flash_messages', locals: { project: @project } -- if @project.repository_exists? && !@project.empty_repo? - - signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @project.default_branch) +- if !@project.empty_repo? && can?(current_user, :download_code, @project) + - signatures_path = project_signatures_path(@project, @project.default_branch) .js-signature-container{ data: { 'signatures-path': signatures_path } } %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 587aeafa82f..5e0523f0b96 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -1,6 +1,6 @@ .tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path } .table-holder - %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" } + %table.table#tree-slider{ class: "table_#{@hex_path} tree-table qa-file-tree" } %thead %tr %th= s_('ProjectFileTree|Name') diff --git a/app/views/projects/wikis/_pages_wiki_page.html.haml b/app/views/projects/wikis/_pages_wiki_page.html.haml index cbb441d7509..c156f8cbf50 100644 --- a/app/views/projects/wikis/_pages_wiki_page.html.haml +++ b/app/views/projects/wikis/_pages_wiki_page.html.haml @@ -2,4 +2,5 @@ = link_to wiki_page.title, project_wiki_path(@project, wiki_page) %small (#{wiki_page.format}) .float-right - %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe + - if wiki_page.last_version + %small= (s_("Last edited %{date}") % { date: time_ago_with_tooltip(wiki_page.last_version.authored_date) }).html_safe diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 19b9744b508..fbf248c2058 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -11,8 +11,9 @@ .nav-text %h2.wiki-page-title= @page.title.capitalize %span.wiki-last-edit-by - = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe - #{time_ago_with_tooltip(@page.last_version.authored_date)} + - if @page.last_version + = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe + #{time_ago_with_tooltip(@page.last_version.authored_date)} .nav-controls = render 'main_links' diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml index 92268e74b1e..a50f1877d08 100644 --- a/app/views/shared/_allow_request_access.html.haml +++ b/app/views/shared/_allow_request_access.html.haml @@ -1,6 +1,8 @@ +- label_class = local_assigns.fetch(:bold_label, false) ? 'font-weight-bold' : '' + .form-check = form.check_box :request_access_enabled, class: 'form-check-input' = form.label :request_access_enabled, class: 'form-check-label' do - %strong Allow users to request access + %span{ class: label_class }= _('Allow users to request access') %br - %span.descr Allow users to request access if visibility is public or internal. + %span.text-muted= _('Allow users to request access if visibility is public or internal.') diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml index b89045e726a..606d0f241aa 100644 --- a/app/views/shared/_field.html.haml +++ b/app/views/shared/_field.html.haml @@ -24,6 +24,6 @@ - elsif type == 'select' = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control", disabled: disabled} - elsif type == 'password' - = form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && required, disabled: disabled + = form.password_field name, autocomplete: "new-password", placeholder: placeholder, class: "form-control", required: value.blank? && required, disabled: disabled - if help %span.form-text.text-muted= help diff --git a/app/views/shared/_old_visibility_level.html.haml b/app/views/shared/_old_visibility_level.html.haml new file mode 100644 index 00000000000..fd576e4fbea --- /dev/null +++ b/app/views/shared/_old_visibility_level.html.haml @@ -0,0 +1,6 @@ +.form-group.row + .col-sm-2.col-form-label + = _('Visibility level') + = link_to icon('question-circle'), help_page_path("public_access/public_access") + .col-sm-10 + = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_visibility_level, form_model: form_model, with_label: with_label diff --git a/app/views/shared/_user_dropdown_contributing_link.html.haml b/app/views/shared/_user_dropdown_contributing_link.html.haml index 333d6fa3489..564d21a39be 100644 --- a/app/views/shared/_user_dropdown_contributing_link.html.haml +++ b/app/views/shared/_user_dropdown_contributing_link.html.haml @@ -1,5 +1,3 @@ %li = link_to "https://about.gitlab.com/contributing", target: '_blank', class: 'text-nowrap' do = _("Contribute to GitLab") - = sprite_icon('external-link', size: 16) -%li.divider diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index ba37b37a3b1..2f42a877beb 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,17 +1,19 @@ - with_label = local_assigns.fetch(:with_label, true) -.form-group.row.visibility-level-setting +.form-group.visibility-level-setting - if with_label - = f.label :visibility_level, class: 'col-form-label col-sm-2 pt-0' do - Visibility Level - = link_to icon('question-circle'), help_page_path("public_access/public_access") - %div{ :class => (with_label ? "col-sm-10" : "col-sm-12") } - - if can_change_visibility_level - = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) - - else - %div - %span.info - = visibility_level_icon(visibility_level) - %strong - = visibility_level_label(visibility_level) - .light= visibility_level_description(visibility_level, form_model) + = f.label :visibility_level, _('Visibility level'), class: 'label-bold append-bottom-0' + %p + = _('Who can see this group?') + - visibility_docs_path = help_page_path('public_access/public_access') + - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: visibility_docs_path } + = s_('Check the %{docs_link_start}documentation%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe } + - if can_change_visibility_level + = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) + - else + %div + %span.info + = visibility_level_icon(visibility_level) + %strong + = visibility_level_label(visibility_level) + .light= visibility_level_description(visibility_level, form_model) diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 6eb1f8f0853..d5fb85ba0f3 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -20,7 +20,7 @@ = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data } + %button.dropdown-menu-toggle.js-label-select.js-multiselect.qa-issuable-label{ class: classes.join(' '), type: "button", data: dropdown_data } - apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles %span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) } = multi_label_name(selected, label_name) diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index aa136af1955..10ffe8dd37f 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -98,7 +98,7 @@ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' - .value.issuable-show-labels.dont-hide.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } + .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any? - selected_labels.each do |label| = link_to_label(label, subject: issuable.project, type: issuable.to_ability_name) diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml index 335c34a4632..7619d0a2e9c 100644 --- a/app/views/shared/labels/_form.html.haml +++ b/app/views/shared/labels/_form.html.haml @@ -4,18 +4,18 @@ .form-group.row = f.label :title, class: 'col-form-label col-sm-2' .col-sm-10 - = f.text_field :title, class: "form-control", required: true, autofocus: true + = f.text_field :title, class: "form-control qa-label-title", required: true, autofocus: true .form-group.row = f.label :description, class: 'col-form-label col-sm-2' .col-sm-10 - = f.text_field :description, class: "form-control js-quick-submit" + = f.text_field :description, class: "form-control js-quick-submit qa-label-description" .form-group.row = f.label :color, "Background color", class: 'col-form-label col-sm-2' .col-sm-10 .input-group .input-group-prepend .input-group-text.label-color-preview - = f.text_field :color, class: "form-control" + = f.text_field :color, class: "form-control qa-label-color" .form-text.text-muted Choose any color. %br @@ -30,5 +30,5 @@ - if @label.persisted? = f.submit 'Save changes', class: 'btn btn-success js-save-button' - else - = f.submit 'Create label', class: 'btn btn-success js-save-button' + = f.submit 'Create label', class: 'btn btn-success js-save-button qa-label-create-button' = link_to 'Cancel', back_path, class: 'btn btn-cancel' diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index e1da05d8f08..06eb3d03e31 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -8,6 +8,7 @@ - user = local_assigns[:user] - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - remote = false unless local_assigns[:remote] == true +- skip_pagination = false unless local_assigns[:skip_pagination] == true .js-projects-list-holder - if any_projects?(projects) @@ -25,6 +26,6 @@ = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon') %strong= pluralize(@private_forks_count, 'private fork') %span you have no access to. - = paginate_collection(projects, remote: remote) + = paginate_collection(projects, remote: remote) unless skip_pagination - else .nothing-here-block No projects found diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 1b66d3acd40..cf9c3055499 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -14,7 +14,7 @@ = render 'shared/form_elements/description', model: @snippet, project: @project, form: f - = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet + = render 'shared/old_visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet, with_label: false .file-editor .form-group.row |