diff options
Diffstat (limited to 'app/assets')
259 files changed, 8449 insertions, 5368 deletions
diff --git a/app/assets/images/new_nav.png b/app/assets/images/new_nav.png Binary files differnew file mode 100644 index 00000000000..f98ca15d787 --- /dev/null +++ b/app/assets/images/new_nav.png diff --git a/app/assets/images/old_nav.png b/app/assets/images/old_nav.png Binary files differnew file mode 100644 index 00000000000..23fae7aa19e --- /dev/null +++ b/app/assets/images/old_nav.png diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index adb45b0606d..18cd04b176a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,12 +1,8 @@ +/* eslint-disable class-methods-use-this */ /* global Flash */ import Cookies from 'js-cookie'; -import emojiMap from 'emojis/digests.json'; -import emojiAliases from 'emojis/aliases.json'; -import { glEmojiTag } from './behaviors/gl_emoji'; -import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid'; - const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; const requestAnimationFrame = window.requestAnimationFrame || @@ -16,8 +12,6 @@ const requestAnimationFrame = window.requestAnimationFrame || const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence -let categoryMap = null; - const categoryLabelMap = { activity: 'Activity', people: 'People', @@ -29,186 +23,144 @@ const categoryLabelMap = { flags: 'Flags', }; -function buildCategoryMap() { - return Object.keys(emojiMap).reduce((currentCategoryMap, emojiNameKey) => { - const emojiInfo = emojiMap[emojiNameKey]; - if (currentCategoryMap[emojiInfo.category]) { - currentCategoryMap[emojiInfo.category].push(emojiNameKey); - } - - return currentCategoryMap; - }, { - activity: [], - people: [], - nature: [], - food: [], - travel: [], - objects: [], - symbols: [], - flags: [], - }); -} - -function renderCategory(name, emojiList, opts = {}) { - return ` - <h5 class="emoji-menu-title"> - ${name} - </h5> - <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}"> - ${emojiList.map(emojiName => ` - <li class="emoji-menu-list-item"> - <button class="emoji-menu-btn text-center js-emoji-btn" type="button"> - ${glEmojiTag(emojiName, { - sprite: true, - })} - </button> - </li> - `).join('\n')} - </ul> - `; -} +class AwardsHandler { + constructor(emoji) { + this.emoji = emoji; + this.eventListeners = []; + // If the user shows intent let's pre-build the menu + this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { + const $menu = $('.emoji-menu'); + if ($menu.length === 0) { + requestAnimationFrame(() => { + this.createEmojiMenu(); + }); + } + }); + this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.showEmojiMenu($(e.currentTarget)); + }); -function AwardsHandler() { - this.eventListeners = []; - this.aliases = emojiAliases; - // If the user shows intent let's pre-build the menu - this.registerEventListener('one', $(document), 'mouseenter focus', '.js-add-award', 'mouseenter focus', () => { - const $menu = $('.emoji-menu'); - if ($menu.length === 0) { - requestAnimationFrame(() => { - this.createEmojiMenu(); - }); - } - // Prebuild the categoryMap - categoryMap = categoryMap || buildCategoryMap(); - }); - this.registerEventListener('on', $(document), 'click', '.js-add-award', (e) => { - e.stopPropagation(); - e.preventDefault(); - this.showEmojiMenu($(e.currentTarget)); - }); - - this.registerEventListener('on', $('html'), 'click', (e) => { - const $target = $(e.target); - if (!$target.closest('.emoji-menu-content').length) { - $('.js-awards-block.current').removeClass('current'); - } - if (!$target.closest('.emoji-menu').length) { - if ($('.emoji-menu').is(':visible')) { - $('.js-add-award.is-active').removeClass('is-active'); - $('.emoji-menu').removeClass('is-visible'); + this.registerEventListener('on', $('html'), 'click', (e) => { + const $target = $(e.target); + if (!$target.closest('.emoji-menu-content').length) { + $('.js-awards-block.current').removeClass('current'); } - } - }); - this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => { - e.preventDefault(); - const $target = $(e.currentTarget); - const $glEmojiElement = $target.find('gl-emoji'); - const $spriteIconElement = $target.find('.icon'); - const emoji = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); - - $target.closest('.js-awards-block').addClass('current'); - this.addAward(this.getVotesBlock(), this.getAwardUrl(), emoji); - }); -} + if (!$target.closest('.emoji-menu').length) { + if ($('.emoji-menu').is(':visible')) { + $('.js-add-award.is-active').removeClass('is-active'); + $('.emoji-menu').removeClass('is-visible'); + } + } + }); + this.registerEventListener('on', $(document), 'click', '.js-emoji-btn', (e) => { + e.preventDefault(); + const $target = $(e.currentTarget); + const $glEmojiElement = $target.find('gl-emoji'); + const $spriteIconElement = $target.find('.icon'); + const emojiName = ($glEmojiElement.length ? $glEmojiElement : $spriteIconElement).data('name'); + + $target.closest('.js-awards-block').addClass('current'); + this.addAward(this.getVotesBlock(), this.getAwardUrl(), emojiName); + }); + } -AwardsHandler.prototype.registerEventListener = function registerEventListener(method = 'on', element, ...args) { - element[method].call(element, ...args); - this.eventListeners.push({ - element, - args, - }); -}; + registerEventListener(method = 'on', element, ...args) { + element[method].call(element, ...args); + this.eventListeners.push({ + element, + args, + }); + } -AwardsHandler.prototype.showEmojiMenu = function showEmojiMenu($addBtn) { - if ($addBtn.hasClass('js-note-emoji')) { - $addBtn.closest('.note').find('.js-awards-block').addClass('current'); - } else { - $addBtn.closest('.js-awards-block').addClass('current'); - } - - const $menu = $('.emoji-menu'); - const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent(); - const $userAuthored = this.isUserAuthored($addBtn); - if ($menu.length) { - if ($menu.is('.is-visible')) { - $addBtn.removeClass('is-active'); - $menu.removeClass('is-visible'); - $('.js-emoji-menu-search').blur(); + showEmojiMenu($addBtn) { + if ($addBtn.hasClass('js-note-emoji')) { + $addBtn.closest('.note').find('.js-awards-block').addClass('current'); } else { - $addBtn.addClass('is-active'); - this.positionMenu($menu, $addBtn); - $menu.addClass('is-visible'); - $('.js-emoji-menu-search').focus(); + $addBtn.closest('.js-awards-block').addClass('current'); } - } else { - $addBtn.addClass('is-loading is-active'); - this.createEmojiMenu(() => { - const $createdMenu = $('.emoji-menu'); - $addBtn.removeClass('is-loading'); - this.positionMenu($createdMenu, $addBtn); - return setTimeout(() => { - $createdMenu.addClass('is-visible'); - $('.js-emoji-menu-search').focus(); - }, 200); - }); - } - $thumbsBtn.toggleClass('disabled', $userAuthored); -}; + const $menu = $('.emoji-menu'); + const $thumbsBtn = $menu.find('[data-name="thumbsup"], [data-name="thumbsdown"]').parent(); + const $userAuthored = this.isUserAuthored($addBtn); + if ($menu.length) { + if ($menu.is('.is-visible')) { + $addBtn.removeClass('is-active'); + $menu.removeClass('is-visible'); + $('.js-emoji-menu-search').blur(); + } else { + $addBtn.addClass('is-active'); + this.positionMenu($menu, $addBtn); + $menu.addClass('is-visible'); + $('.js-emoji-menu-search').focus(); + } + } else { + $addBtn.addClass('is-loading is-active'); + this.createEmojiMenu(() => { + const $createdMenu = $('.emoji-menu'); + $addBtn.removeClass('is-loading'); + this.positionMenu($createdMenu, $addBtn); + return setTimeout(() => { + $createdMenu.addClass('is-visible'); + $('.js-emoji-menu-search').focus(); + }, 200); + }); + } -// Create the emoji menu with the first category of emojis. -// Then render the remaining categories of emojis one by one to avoid jank. -AwardsHandler.prototype.createEmojiMenu = function createEmojiMenu(callback) { - if (this.isCreatingEmojiMenu) { - return; - } - this.isCreatingEmojiMenu = true; - - // Render the first category - categoryMap = categoryMap || buildCategoryMap(); - const categoryNameKey = Object.keys(categoryMap)[0]; - const emojisInCategory = categoryMap[categoryNameKey]; - const firstCategory = renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); - - // Render the frequently used - const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); - let frequentlyUsedCatgegory = ''; - if (frequentlyUsedEmojis.length > 0) { - frequentlyUsedCatgegory = renderCategory('Frequently used', frequentlyUsedEmojis, { - menuListClass: 'frequent-emojis', - }); + $thumbsBtn.toggleClass('disabled', $userAuthored); } - const emojiMenuMarkup = ` - <div class="emoji-menu"> - <input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" /> + // Create the emoji menu with the first category of emojis. + // Then render the remaining categories of emojis one by one to avoid jank. + createEmojiMenu(callback) { + if (this.isCreatingEmojiMenu) { + return; + } + this.isCreatingEmojiMenu = true; + + // Render the first category + const categoryMap = this.emoji.getEmojiCategoryMap(); + const categoryNameKey = Object.keys(categoryMap)[0]; + const emojisInCategory = categoryMap[categoryNameKey]; + const firstCategory = this.renderCategory(categoryLabelMap[categoryNameKey], emojisInCategory); + + // Render the frequently used + const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); + let frequentlyUsedCatgegory = ''; + if (frequentlyUsedEmojis.length > 0) { + frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, { + menuListClass: 'frequent-emojis', + }); + } + + const emojiMenuMarkup = ` + <div class="emoji-menu"> + <input type="text" name="emoji-menu-search" value="" class="js-emoji-menu-search emoji-search search-input form-control" placeholder="Search emoji" /> - <div class="emoji-menu-content"> - ${frequentlyUsedCatgegory} - ${firstCategory} + <div class="emoji-menu-content"> + ${frequentlyUsedCatgegory} + ${firstCategory} + </div> </div> - </div> - `; + `; - document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); + document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); - this.addRemainingEmojiMenuCategories(); - this.setupSearch(); - if (callback) { - callback(); + this.addRemainingEmojiMenuCategories(); + this.setupSearch(); + if (callback) { + callback(); + } } -}; -AwardsHandler - .prototype - .addRemainingEmojiMenuCategories = function addRemainingEmojiMenuCategories() { + addRemainingEmojiMenuCategories() { if (this.isAddingRemainingEmojiMenuCategories) { return; } this.isAddingRemainingEmojiMenuCategories = true; - categoryMap = categoryMap || buildCategoryMap(); + const categoryMap = this.emoji.getEmojiCategoryMap(); // Avoid the jank and render the remaining categories separately // This will take more time, but makes UI more responsive @@ -220,7 +172,7 @@ AwardsHandler promiseChain.then(() => new Promise((resolve) => { const emojisInCategory = categoryMap[categoryNameKey]; - const categoryMarkup = renderCategory( + const categoryMarkup = this.renderCategory( categoryLabelMap[categoryNameKey], emojisInCategory, ); @@ -243,179 +195,186 @@ AwardsHandler emojiContentElement.insertAdjacentHTML('beforeend', '<p>We encountered an error while adding the remaining categories</p>'); throw new Error(`Error occurred in addRemainingEmojiMenuCategories: ${err.message}`); }); - }; - -AwardsHandler.prototype.positionMenu = function positionMenu($menu, $addBtn) { - const position = $addBtn.data('position'); - // The menu could potentially be off-screen or in a hidden overflow element - // So we position the element absolute in the body - const css = { - top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`, - }; - if (position === 'right') { - css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`; - $menu.addClass('is-aligned-right'); - } else { - css.left = `${$addBtn.offset().left}px`; - $menu.removeClass('is-aligned-right'); - } - return $menu.css(css); -}; + } -AwardsHandler.prototype.addAward = function addAward( - votesBlock, - awardUrl, - emoji, - checkMutuality, - callback, -) { - const normalizedEmoji = this.normalizeEmojiName(emoji); - const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); - this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { - this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); - return typeof callback === 'function' ? callback() : undefined; - }); - $('.emoji-menu').removeClass('is-visible'); - $('.js-add-award.is-active').removeClass('is-active'); -}; + renderCategory(name, emojiList, opts = {}) { + return ` + <h5 class="emoji-menu-title"> + ${name} + </h5> + <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}"> + ${emojiList.map(emojiName => ` + <li class="emoji-menu-list-item"> + <button class="emoji-menu-btn text-center js-emoji-btn" type="button"> + ${this.emoji.glEmojiTag(emojiName, { + sprite: true, + })} + </button> + </li> + `).join('\n')} + </ul> + `; + } -AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar( - votesBlock, - emoji, - checkForMutuality, -) { - if (checkForMutuality || checkForMutuality === null) { - this.checkMutuality(votesBlock, emoji); - } - this.addEmojiToFrequentlyUsedList(emoji); - const normalizedEmoji = this.normalizeEmojiName(emoji); - const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); - if ($emojiButton.length > 0) { - if (this.isActive($emojiButton)) { - this.decrementCounter($emojiButton, normalizedEmoji); + positionMenu($menu, $addBtn) { + const position = $addBtn.data('position'); + // The menu could potentially be off-screen or in a hidden overflow element + // So we position the element absolute in the body + const css = { + top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`, + }; + if (position === 'right') { + css.left = `${($addBtn.offset().left - $menu.outerWidth()) + 20}px`; + $menu.addClass('is-aligned-right'); } else { - const counter = $emojiButton.find('.js-counter'); - counter.text(parseInt(counter.text(), 10) + 1); - $emojiButton.addClass('active'); - this.addYouToUserList(votesBlock, normalizedEmoji); - this.animateEmoji($emojiButton); + css.left = `${$addBtn.offset().left}px`; + $menu.removeClass('is-aligned-right'); } - } else { - votesBlock.removeClass('hidden'); - this.createEmoji(votesBlock, normalizedEmoji); + return $menu.css(css); } -}; -AwardsHandler.prototype.getVotesBlock = function getVotesBlock() { - const currentBlock = $('.js-awards-block.current'); - let resultantVotesBlock = currentBlock; - if (currentBlock.length === 0) { - resultantVotesBlock = $('.js-awards-block').eq(0); + addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { + const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); + const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); + this.postEmoji($emojiButton, awardUrl, normalizedEmoji, () => { + this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); + return typeof callback === 'function' ? callback() : undefined; + }); + $('.emoji-menu').removeClass('is-visible'); + $('.js-add-award.is-active').removeClass('is-active'); } - return resultantVotesBlock; -}; + addAwardToEmojiBar(votesBlock, emoji, checkForMutuality) { + if (checkForMutuality || checkForMutuality === null) { + this.checkMutuality(votesBlock, emoji); + } + this.addEmojiToFrequentlyUsedList(emoji); + const normalizedEmoji = this.emoji.normalizeEmojiName(emoji); + const $emojiButton = this.findEmojiIcon(votesBlock, normalizedEmoji).parent(); + if ($emojiButton.length > 0) { + if (this.isActive($emojiButton)) { + this.decrementCounter($emojiButton, normalizedEmoji); + } else { + const counter = $emojiButton.find('.js-counter'); + counter.text(parseInt(counter.text(), 10) + 1); + $emojiButton.addClass('active'); + this.addYouToUserList(votesBlock, normalizedEmoji); + this.animateEmoji($emojiButton); + } + } else { + votesBlock.removeClass('hidden'); + this.createEmoji(votesBlock, normalizedEmoji); + } + } -AwardsHandler.prototype.getAwardUrl = function getAwardUrl() { - return this.getVotesBlock().data('award-url'); -}; + getVotesBlock() { + const currentBlock = $('.js-awards-block.current'); + let resultantVotesBlock = currentBlock; + if (currentBlock.length === 0) { + resultantVotesBlock = $('.js-awards-block').eq(0); + } -AwardsHandler.prototype.checkMutuality = function checkMutuality(votesBlock, emoji) { - const awardUrl = this.getAwardUrl(); - if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; - const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent(); - const isAlreadyVoted = $emojiButton.hasClass('active'); - if (isAlreadyVoted) { - this.addAward(votesBlock, awardUrl, mutualVote, false); + return resultantVotesBlock; + } + + getAwardUrl() { + return this.getVotesBlock().data('award-url'); + } + + checkMutuality(votesBlock, emoji) { + const awardUrl = this.getAwardUrl(); + if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup'; + const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).parent(); + const isAlreadyVoted = $emojiButton.hasClass('active'); + if (isAlreadyVoted) { + this.addAward(votesBlock, awardUrl, mutualVote, false); + } } } -}; -AwardsHandler.prototype.isActive = function isActive($emojiButton) { - return $emojiButton.hasClass('active'); -}; + isActive($emojiButton) { + return $emojiButton.hasClass('active'); + } -AwardsHandler.prototype.isUserAuthored = function isUserAuthored($button) { - return $button.hasClass('js-user-authored'); -}; + isUserAuthored($button) { + return $button.hasClass('js-user-authored'); + } -AwardsHandler.prototype.decrementCounter = function decrementCounter($emojiButton, emoji) { - const counter = $('.js-counter', $emojiButton); - const counterNumber = parseInt(counter.text(), 10); - if (counterNumber > 1) { - counter.text(counterNumber - 1); - this.removeYouFromUserList($emojiButton); - } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { - $emojiButton.tooltip('destroy'); - counter.text('0'); - this.removeYouFromUserList($emojiButton); - if ($emojiButton.parents('.note').length) { + decrementCounter($emojiButton, emoji) { + const counter = $('.js-counter', $emojiButton); + const counterNumber = parseInt(counter.text(), 10); + if (counterNumber > 1) { + counter.text(counterNumber - 1); + this.removeYouFromUserList($emojiButton); + } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') { + $emojiButton.tooltip('destroy'); + counter.text('0'); + this.removeYouFromUserList($emojiButton); + if ($emojiButton.parents('.note').length) { + this.removeEmoji($emojiButton); + } + } else { this.removeEmoji($emojiButton); } - } else { - this.removeEmoji($emojiButton); + return $emojiButton.removeClass('active'); } - return $emojiButton.removeClass('active'); -}; -AwardsHandler.prototype.removeEmoji = function removeEmoji($emojiButton) { - $emojiButton.tooltip('destroy'); - $emojiButton.remove(); - const $votesBlock = this.getVotesBlock(); - if ($votesBlock.find('.js-emoji-btn').length === 0) { - $votesBlock.addClass('hidden'); + removeEmoji($emojiButton) { + $emojiButton.tooltip('destroy'); + $emojiButton.remove(); + const $votesBlock = this.getVotesBlock(); + if ($votesBlock.find('.js-emoji-btn').length === 0) { + $votesBlock.addClass('hidden'); + } } -}; -AwardsHandler.prototype.getAwardTooltip = function getAwardTooltip($awardBlock) { - return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; -}; - -AwardsHandler.prototype.toSentence = function toSentence(list) { - let sentence; - if (list.length <= 2) { - sentence = list.join(' and '); - } else { - sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`; + getAwardTooltip($awardBlock) { + return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || ''; } - return sentence; -}; + toSentence(list) { + let sentence; + if (list.length <= 2) { + sentence = list.join(' and '); + } else { + sentence = `${list.slice(0, -1).join(', ')}, and ${list[list.length - 1]}`; + } -AwardsHandler.prototype.removeYouFromUserList = function removeYouFromUserList($emojiButton) { - const awardBlock = $emojiButton; - const originalTitle = this.getAwardTooltip(awardBlock); - const authors = originalTitle.split(FROM_SENTENCE_REGEX); - authors.splice(authors.indexOf('You'), 1); - return awardBlock - .closest('.js-emoji-btn') - .removeData('title') - .removeAttr('data-title') - .removeAttr('data-original-title') - .attr('title', this.toSentence(authors)) - .tooltip('fixTitle'); -}; + return sentence; + } -AwardsHandler.prototype.addYouToUserList = function addYouToUserList(votesBlock, emoji) { - const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); - const origTitle = this.getAwardTooltip(awardBlock); - let users = []; - if (origTitle) { - users = origTitle.trim().split(FROM_SENTENCE_REGEX); - } - users.unshift('You'); - return awardBlock - .attr('title', this.toSentence(users)) - .tooltip('fixTitle'); -}; + removeYouFromUserList($emojiButton) { + const awardBlock = $emojiButton; + const originalTitle = this.getAwardTooltip(awardBlock); + const authors = originalTitle.split(FROM_SENTENCE_REGEX); + authors.splice(authors.indexOf('You'), 1); + return awardBlock + .closest('.js-emoji-btn') + .removeData('title') + .removeAttr('data-title') + .removeAttr('data-original-title') + .attr('title', this.toSentence(authors)) + .tooltip('fixTitle'); + } + + addYouToUserList(votesBlock, emoji) { + const awardBlock = this.findEmojiIcon(votesBlock, emoji).parent(); + const origTitle = this.getAwardTooltip(awardBlock); + let users = []; + if (origTitle) { + users = origTitle.trim().split(FROM_SENTENCE_REGEX); + } + users.unshift('You'); + return awardBlock + .attr('title', this.toSentence(users)) + .tooltip('fixTitle'); + } -AwardsHandler - .prototype - .createAwardButtonForVotesBlock = function createAwardButtonForVotesBlock(votesBlock, emojiName) { + createAwardButtonForVotesBlock(votesBlock, emojiName) { const buttonHtml = ` <button class="btn award-control js-emoji-btn has-tooltip active" title="You" data-placement="bottom"> - ${glEmojiTag(emojiName)} + ${this.emoji.glEmojiTag(emojiName)} <span class="award-control-text js-counter">1</span> </button> `; @@ -424,144 +383,136 @@ AwardsHandler this.animateEmoji($emojiButton); $('.award-control').tooltip(); votesBlock.removeClass('current'); - }; - -AwardsHandler.prototype.animateEmoji = function animateEmoji($emoji) { - const className = 'pulse animated once short'; - $emoji.addClass(className); + } - this.registerEventListener('on', $emoji, animationEndEventString, (e) => { - $(e.currentTarget).removeClass(className); - }); -}; + animateEmoji($emoji) { + const className = 'pulse animated once short'; + $emoji.addClass(className); -AwardsHandler.prototype.createEmoji = function createEmoji(votesBlock, emoji) { - if ($('.emoji-menu').length) { - this.createAwardButtonForVotesBlock(votesBlock, emoji); + this.registerEventListener('on', $emoji, animationEndEventString, (e) => { + $(e.currentTarget).removeClass(className); + }); } - this.createEmojiMenu(() => { - this.createAwardButtonForVotesBlock(votesBlock, emoji); - }); -}; -AwardsHandler.prototype.postEmoji = function postEmoji($emojiButton, awardUrl, emoji, callback) { - if (this.isUserAuthored($emojiButton)) { - this.userAuthored($emojiButton); - } else { - $.post(awardUrl, { - name: emoji, - }, (data) => { - if (data.ok) { - callback(); - } - }).fail(() => new Flash('Something went wrong on our end.')); + createEmoji(votesBlock, emoji) { + if ($('.emoji-menu').length) { + this.createAwardButtonForVotesBlock(votesBlock, emoji); + } + this.createEmojiMenu(() => { + this.createAwardButtonForVotesBlock(votesBlock, emoji); + }); } -}; -AwardsHandler.prototype.findEmojiIcon = function findEmojiIcon(votesBlock, emoji) { - return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); -}; + postEmoji($emojiButton, awardUrl, emoji, callback) { + if (this.isUserAuthored($emojiButton)) { + this.userAuthored($emojiButton); + } else { + $.post(awardUrl, { + name: emoji, + }, (data) => { + if (data.ok) { + callback(); + } + }).fail(() => new Flash('Something went wrong on our end.')); + } + } -AwardsHandler.prototype.userAuthored = function userAuthored($emojiButton) { - const oldTitle = this.getAwardTooltip($emojiButton); - const newTitle = 'You cannot vote on your own issue, MR and note'; - gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show'); - // Restore tooltip back to award list - return setTimeout(() => { - $emojiButton.tooltip('hide'); - gl.utils.updateTooltipTitle($emojiButton, oldTitle); - }, 2800); -}; + findEmojiIcon(votesBlock, emoji) { + return votesBlock.find(`.js-emoji-btn [data-name="${emoji}"]`); + } -AwardsHandler.prototype.scrollToAwards = function scrollToAwards() { - const options = { - scrollTop: $('.awards').offset().top - 110, - }; - return $('body, html').animate(options, 200); -}; + userAuthored($emojiButton) { + const oldTitle = this.getAwardTooltip($emojiButton); + const newTitle = 'You cannot vote on your own issue, MR and note'; + gl.utils.updateTooltipTitle($emojiButton, newTitle).tooltip('show'); + // Restore tooltip back to award list + return setTimeout(() => { + $emojiButton.tooltip('hide'); + gl.utils.updateTooltipTitle($emojiButton, oldTitle); + }, 2800); + } -AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji) { - return Object.prototype.hasOwnProperty.call(this.aliases, emoji) ? this.aliases[emoji] : emoji; -}; + scrollToAwards() { + const options = { + scrollTop: $('.awards').offset().top - 110, + }; + return $('body, html').animate(options, 200); + } -AwardsHandler - .prototype - .addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) { - if (isEmojiNameValid(emoji)) { + addEmojiToFrequentlyUsedList(emoji) { + if (this.emoji.isEmojiNameValid(emoji)) { this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji)); Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 }); } - }; - -AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() { - return this.frequentlyUsedEmojis || (() => { - const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); - this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( - inputName => isEmojiNameValid(inputName), - ); + } - return this.frequentlyUsedEmojis; - })(); -}; + getFrequentlyUsedEmojis() { + return this.frequentlyUsedEmojis || (() => { + const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(',')); + this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter( + inputName => this.emoji.isEmojiNameValid(inputName), + ); -AwardsHandler.prototype.setupSearch = function setupSearch() { - const $search = $('.js-emoji-menu-search'); + return this.frequentlyUsedEmojis; + })(); + } - this.registerEventListener('on', $search, 'input', (e) => { - const term = $(e.target).val().trim(); - this.searchEmojis(term); - }); + setupSearch() { + const $search = $('.js-emoji-menu-search'); - const $menu = $('.emoji-menu'); - this.registerEventListener('on', $menu, transitionEndEventString, (e) => { - if (e.target === e.currentTarget) { - // Clear the search - this.searchEmojis(''); - } - }); -}; + this.registerEventListener('on', $search, 'input', (e) => { + const term = $(e.target).val().trim(); + this.searchEmojis(term); + }); -AwardsHandler.prototype.searchEmojis = function searchEmojis(term) { - const $search = $('.js-emoji-menu-search'); - $search.val(term); - - // Clean previous search results - $('ul.emoji-menu-search, h5.emoji-search-title').remove(); - if (term.length > 0) { - // Generate a search result block - const h5 = $('<h5 class="emoji-search-title"/>').text('Search results'); - const foundEmojis = this.findMatchingEmojiElements(term).show(); - const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); - $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); - $('.emoji-menu-content').append(h5).append(ul); - } else { - $('.emoji-menu-content').children().show(); + const $menu = $('.emoji-menu'); + this.registerEventListener('on', $menu, transitionEndEventString, (e) => { + if (e.target === e.currentTarget) { + // Clear the search + this.searchEmojis(''); + } + }); } -}; - -AwardsHandler.prototype.findMatchingEmojiElements = function findMatchingEmojiElements(term) { - const safeTerm = term.toLowerCase(); - const namesMatchingAlias = []; - Object.keys(emojiAliases).forEach((alias) => { - if (alias.indexOf(safeTerm) >= 0) { - namesMatchingAlias.push(emojiAliases[alias]); + searchEmojis(term) { + const $search = $('.js-emoji-menu-search'); + $search.val(term); + + // Clean previous search results + $('ul.emoji-menu-search, h5.emoji-search-title').remove(); + if (term.length > 0) { + // Generate a search result block + const h5 = $('<h5 class="emoji-search-title"/>').text('Search results'); + const foundEmojis = this.findMatchingEmojiElements(term).show(); + const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); + $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); + $('.emoji-menu-content').append(h5).append(ul); + } else { + $('.emoji-menu-content').children().show(); } - }); - const $matchingElements = namesMatchingAlias.concat(safeTerm) - .reduce( - ($result, searchTerm) => - $result.add($(`.emoji-menu-list:not(.frequent-emojis) [data-name*="${searchTerm}"]`)), - $([]), - ); - return $matchingElements.closest('li').clone(); -}; + } -AwardsHandler.prototype.destroy = function destroy() { - this.eventListeners.forEach((entry) => { - entry.element.off.call(entry.element, ...entry.args); - }); - $('.emoji-menu').remove(); -}; + findMatchingEmojiElements(query) { + const emojiMatches = this.emoji.filterEmojiNamesByAlias(query); + const $emojiElements = $('.emoji-menu-list:not(.frequent-emojis) [data-name]'); + const $matchingElements = $emojiElements + .filter((i, elm) => emojiMatches.indexOf(elm.dataset.name) >= 0); + return $matchingElements.closest('li').clone(); + } + + destroy() { + this.eventListeners.forEach((entry) => { + entry.element.off.call(entry.element, ...entry.args); + }); + $('.emoji-menu').remove(); + } +} -export default AwardsHandler; +let awardsHandlerPromise = null; +export default function loadAwardsHandler(reload = false) { + if (!awardsHandlerPromise || reload) { + awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji') + .then(Emoji => new AwardsHandler(Emoji)); + } + return awardsHandlerPromise; +} diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index 3bea460dcc6..e00af4b2fa8 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,23 +1,8 @@ import autosize from 'vendor/autosize'; -$(() => { - const $fields = $('.js-autosize'); +document.addEventListener('DOMContentLoaded', () => { + const autosizeEls = document.querySelectorAll('.js-autosize'); - $fields.on('autosize:resized', function resized() { - const $field = $(this); - $field.data('height', $field.outerHeight()); - }); - - $fields.on('resize.autosize', function resize() { - const $field = $(this); - if ($field.data('height') !== $field.outerHeight()) { - $field.data('height', $field.outerHeight()); - autosize.destroy($field); - $field.css('max-height', window.outerHeight); - } - }); - - autosize($fields); - autosize.update($fields); - $fields.css('resize', 'vertical'); + autosize(autosizeEls); + autosize.update(autosizeEls); }); diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index 36ce4fddb72..7e98e04303a 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -1,75 +1,9 @@ import installCustomElements from 'document-register-element'; -import emojiMap from 'emojis/digests.json'; -import emojiAliases from 'emojis/aliases.json'; -import { getUnicodeSupportMap } from './gl_emoji/unicode_support_map'; -import { isEmojiUnicodeSupported } from './gl_emoji/is_emoji_unicode_supported'; +import isEmojiUnicodeSupported from '../emoji/support'; installCustomElements(window); -const generatedUnicodeSupportMap = getUnicodeSupportMap(); - -function emojiImageTag(name, src) { - return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`; -} - -function assembleFallbackImageSrc(inputName) { - let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? - emojiAliases[inputName] : inputName; - let emojiInfo = emojiMap[name]; - // Fallback to question mark for unknown emojis - if (!emojiInfo) { - name = 'grey_question'; - emojiInfo = emojiMap[name]; - } - const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`; - - return fallbackImageSrc; -} -const glEmojiTagDefaults = { - sprite: false, - forceFallback: false, -}; -function glEmojiTag(inputName, options) { - const opts = Object.assign({}, glEmojiTagDefaults, options); - let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? - emojiAliases[inputName] : inputName; - let emojiInfo = emojiMap[name]; - // Fallback to question mark for unknown emojis - if (!emojiInfo) { - name = 'grey_question'; - emojiInfo = emojiMap[name]; - } - - const fallbackImageSrc = assembleFallbackImageSrc(name); - const fallbackSpriteClass = `emoji-${name}`; - - const classList = []; - if (opts.forceFallback && opts.sprite) { - classList.push('emoji-icon'); - classList.push(fallbackSpriteClass); - } - const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : ''; - const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : ''; - let contents = emojiInfo.moji; - if (opts.forceFallback && !opts.sprite) { - contents = emojiImageTag(name, fallbackImageSrc); - } - - return ` - <gl-emoji - ${classAttribute} - data-name="${name}" - data-fallback-src="${fallbackImageSrc}" - ${fallbackSpriteAttribute} - data-unicode-version="${emojiInfo.unicodeVersion}" - title="${emojiInfo.description}" - > - ${contents} - </gl-emoji> - `; -} - -function installGlEmojiElement() { +export default function installGlEmojiElement() { const GlEmojiElementProto = Object.create(HTMLElement.prototype); GlEmojiElementProto.createdCallback = function createdCallback() { const emojiUnicode = this.textContent.trim(); @@ -90,18 +24,26 @@ function installGlEmojiElement() { if ( emojiUnicode && isEmojiUnicode && - !isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion) + !isEmojiUnicodeSupported(emojiUnicode, unicodeVersion) ) { // CSS sprite fallback takes precedence over image fallback if (hasCssSpriteFalback) { // IE 11 doesn't like adding multiple at once :( this.classList.add('emoji-icon'); this.classList.add(fallbackSpriteClass); - } else if (hasImageFallback) { - this.innerHTML = emojiImageTag(name, fallbackSrc); } else { - const src = assembleFallbackImageSrc(name); - this.innerHTML = emojiImageTag(name, src); + import(/* webpackChunkName: 'emoji' */ '../emoji') + .then(({ emojiImageTag, emojiFallbackImageSrc }) => { + if (hasImageFallback) { + this.innerHTML = emojiImageTag(name, fallbackSrc); + } else { + const src = emojiFallbackImageSrc(name); + this.innerHTML = emojiImageTag(name, src); + } + }) + .catch(() => { + // do nothing + }); } } }; @@ -110,9 +52,3 @@ function installGlEmojiElement() { prototype: GlEmojiElementProto, }); } - -export { - installGlEmojiElement, - glEmojiTag, - emojiImageTag, -}; diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js deleted file mode 100644 index be4aeb32c46..00000000000 --- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js +++ /dev/null @@ -1,11 +0,0 @@ -import emojiMap from 'emojis/digests.json'; -import emojiAliases from 'emojis/aliases.json'; - -function isEmojiNameValid(inputName) { - const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ? - emojiAliases[inputName] : inputName; - - return name && emojiMap[name]; -} - -export default isEmojiNameValid; diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 5b931e6cfa6..44b2c974b9e 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -1,7 +1,7 @@ import './autosize'; import './bind_in_out'; import './details_behavior'; -import { installGlEmojiElement } from './gl_emoji'; +import installGlEmojiElement from './gl_emoji'; import './quick_submit'; import './requires_input'; import './toggler_behavior'; diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 1f9e0448084..bc693616460 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -40,7 +40,7 @@ $(document).on('keydown.quick_submit', '.js-quick-submit', (e) => { e.preventDefault(); const $form = $(e.target).closest('form'); - const $submitButton = $form.find('input[type=submit], button[type=submit]'); + const $submitButton = $form.find('input[type=submit], button[type=submit]').first(); if (!$submitButton.attr('disabled')) { $submitButton.trigger('click', [e]); diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js index 36fe8a7184f..27312d718b0 100644 --- a/app/assets/javascripts/blob/notebook/index.js +++ b/app/assets/javascripts/blob/notebook/index.js @@ -51,8 +51,9 @@ export default () => { methods: { loadFile() { this.$http.get(el.dataset.endpoint) + .then(response => response.json()) .then((res) => { - this.json = res.json(); + this.json = res; this.loading = false; }) .catch((e) => { diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index b94009ee76b..88b054b76e6 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -81,8 +81,9 @@ $(() => { mounted () { Store.disabled = this.disabled; gl.boardService.all() + .then(response => response.json()) .then((resp) => { - resp.json().forEach((board) => { + resp.forEach((board) => { const list = Store.addList(board, this.defaultAvatar); if (list.type === 'closed') { @@ -97,7 +98,8 @@ $(() => { Store.addBlankState(); this.loading = false; - }).catch(() => new Flash('An error occurred. Please try again.')); + }) + .catch(() => new Flash('An error occurred. Please try again.')); }, methods: { updateTokens() { diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js index 870e115bd1a..e7f16899362 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js +++ b/app/assets/javascripts/boards/components/board_blank_state.js @@ -64,8 +64,9 @@ export default { // Save the labels gl.boardService.generateDefaultLists() - .then((resp) => { - resp.json().forEach((listObj) => { + .then(resp => resp.json()) + .then((data) => { + data.forEach((listObj) => { const list = Store.findList('title', listObj.title); list.id = listObj.id; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js index b1c47b09c35..4af8b0c7713 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js +++ b/app/assets/javascripts/boards/components/board_new_issue.js @@ -17,7 +17,7 @@ export default { methods: { submit(e) { e.preventDefault(); - if (this.title.trim() === '') return; + if (this.title.trim() === '') return Promise.resolve(); this.error = false; @@ -29,7 +29,10 @@ export default { assignees: [], }); - this.list.newIssue(issue) + eventHub.$emit(`scroll-board-list-${this.list.id}`); + this.cancel(); + + return this.list.newIssue(issue) .then(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions $(this.$refs.submitButton).enable(); @@ -47,9 +50,6 @@ export default { // Show error message this.error = true; }); - - eventHub.$emit(`scroll-board-list-${this.list.id}`); - this.cancel(); }, cancel() { this.title = ''; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index c7afd4ead6b..590b7be36e3 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({ }, milestoneTitle() { return this.issue.milestone ? this.issue.milestone.title : 'No Milestone'; - } + }, + canRemove() { + return !this.list.preset; + }, }, watch: { detail: { diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index 6356c266ee2..1d36519c75c 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -88,9 +88,9 @@ gl.issueBoards.IssuesModal = Vue.extend({ return gl.boardService.getBacklog(queryData(this.filter.path, { page: this.page, per: this.perPage, - })).then((res) => { - const data = res.json(); - + })) + .then(resp => resp.json()) + .then((data) => { if (clearIssues) { this.issues = []; } diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 5597f128b80..6a900d4abd0 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ }, template: ` <div - class="block list" - v-if="list.type !== 'closed'"> + class="block list"> <button class="btn btn-default btn-block" type="button" diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 548de1a4c52..08f7c5ddcd2 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -40,9 +40,8 @@ class List { save () { return gl.boardService.createList(this.label.id) - .then((resp) => { - const data = resp.json(); - + .then(resp => resp.json()) + .then((data) => { this.id = data.id; this.type = data.list_type; this.position = data.position; @@ -91,8 +90,8 @@ class List { } return gl.boardService.getIssuesForList(this.id, data) - .then((resp) => { - const data = resp.json(); + .then(resp => resp.json()) + .then((data) => { this.loading = false; this.issuesSize = data.size; @@ -109,11 +108,10 @@ class List { this.issuesSize += 1; return gl.boardService.newIssue(this.id, issue) - .then((resp) => { - const data = resp.json(); + .then(resp => resp.json()) + .then((data) => { issue.id = data.iid; - }) - .then(() => { + if (this.issuesSize > 1) { const moveBeforeIid = this.issues[1].id; gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid); diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index db9bced2f89..3742507b236 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -23,11 +23,6 @@ class BoardService { url: bulkUpdatePath, }, }); - - Vue.http.interceptors.push((request, next) => { - request.headers['X-CSRF-Token'] = $.rails.csrfToken(); - next(); - }); } all () { diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index c28f6e151a0..1dfa064acfd 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -13,25 +13,21 @@ window.Build = (function () { this.options = options || $('.js-build-options').data(); this.pageUrl = this.options.pageUrl; - this.buildUrl = this.options.buildUrl; this.buildStatus = this.options.buildStatus; this.state = this.options.logState; this.buildStage = this.options.buildStage; this.$document = $(document); this.logBytes = 0; - this.scrollOffsetPadding = 30; this.hasBeenScrolled = false; this.updateDropdown = this.updateDropdown.bind(this); this.getBuildTrace = this.getBuildTrace.bind(this); - this.scrollToBottom = this.scrollToBottom.bind(this); - this.$body = $('body'); this.$buildTrace = $('#build-trace'); this.$buildRefreshAnimation = $('.js-build-refresh'); this.$truncatedInfo = $('.js-truncated-info'); this.$buildTraceOutput = $('.js-build-output'); - this.$scrollContainer = $('.js-scroll-container'); + this.$topBar = $('.js-top-bar'); // Scroll controllers this.$scrollTopBtn = $('.js-scroll-up'); @@ -63,13 +59,22 @@ window.Build = (function () { .off('click') .on('click', this.scrollToBottom.bind(this)); - const scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); + this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); - this.$scrollContainer + $(window) .off('scroll') .on('scroll', () => { - this.hasBeenScrolled = true; - scrollThrottled(); + const contentHeight = this.$buildTraceOutput.prop('scrollHeight'); + if (contentHeight > this.windowSize) { + // means the user did not scroll, the content was updated. + this.windowSize = contentHeight; + } else { + // User scrolled + this.hasBeenScrolled = true; + this.toggleScrollAnimation(false); + } + + this.scrollThrottled(); }); $(window) @@ -77,60 +82,73 @@ window.Build = (function () { .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); this.updateArtifactRemoveDate(); + this.initAffixTopArea(); - // eslint-disable-next-line - this.getBuildTrace() - .then(() => this.toggleScroll()) - .then(() => { - if (!this.hasBeenScrolled) { - this.scrollToBottom(); - } - }); - - this.verifyTopPosition(); + this.getBuildTrace(); } + Build.prototype.initAffixTopArea = function () { + /** + If the browser does not support position sticky, it returns the position as static. + If the browser does support sticky, then we allow the browser to handle it, if not + then we default back to Bootstraps affix + **/ + if (this.$topBar.css('position') !== 'static') return; + + const offsetTop = this.$buildTrace.offset().top; + + this.$topBar.affix({ + offset: { + top: offsetTop, + }, + }); + }; + Build.prototype.canScroll = function () { - return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height(); + return document.body.scrollHeight > window.innerHeight; }; - /** - * | | Up | Down | - * |--------------------------|----------|----------| - * | on scroll bottom | active | disabled | - * | on scroll top | disabled | active | - * | no scroll | disabled | disabled | - * | on.('scroll') is on top | disabled | active | - * | on('scroll) is on bottom | active | disabled | - * - */ Build.prototype.toggleScroll = function () { - const currentPosition = this.$scrollContainer.scrollTop(); - const bottomScroll = currentPosition + this.$scrollContainer.innerHeight(); + const currentPosition = document.body.scrollTop; + const windowHeight = window.innerHeight; if (this.canScroll()) { - if (currentPosition === 0) { + if (currentPosition > 0 && + (document.body.scrollHeight - currentPosition !== windowHeight)) { + // User is in the middle of the log + + this.toggleDisableButton(this.$scrollTopBtn, false); + this.toggleDisableButton(this.$scrollBottomBtn, false); + } else if (currentPosition === 0) { + // User is at Top of Build Log + this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollBottomBtn, false); - } else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) { + } else if (document.body.scrollHeight - currentPosition === windowHeight) { + // User is at the bottom of the build log. + this.toggleDisableButton(this.$scrollTopBtn, false); this.toggleDisableButton(this.$scrollBottomBtn, true); - } else { - this.toggleDisableButton(this.$scrollTopBtn, false); - this.toggleDisableButton(this.$scrollBottomBtn, false); } + } else { + this.toggleDisableButton(this.$scrollTopBtn, true); + this.toggleDisableButton(this.$scrollBottomBtn, true); } }; - Build.prototype.scrollToTop = function () { + Build.prototype.scrollDown = function () { + document.body.scrollTop = document.body.scrollHeight; + }; + + Build.prototype.scrollToBottom = function () { + this.scrollDown(); this.hasBeenScrolled = true; - this.$scrollContainer.scrollTop(0); this.toggleScroll(); }; - Build.prototype.scrollToBottom = function () { + Build.prototype.scrollToTop = function () { + document.body.scrollTop = 0; this.hasBeenScrolled = true; - this.$scrollContainer.scrollTop(this.$scrollContainer.prop('scrollHeight')); this.toggleScroll(); }; @@ -143,47 +161,6 @@ window.Build = (function () { this.$scrollBottomBtn.toggleClass('animate', toggle); }; - /** - * Build trace top position depends on the space ocupied by the elments rendered before - */ - Build.prototype.verifyTopPosition = function () { - const $buildPage = $('.build-page'); - - const $flashError = $('.alert-wrapper'); - const $header = $('.build-header', $buildPage); - const $runnersStuck = $('.js-build-stuck', $buildPage); - const $startsEnvironment = $('.js-environment-container', $buildPage); - const $erased = $('.js-build-erased', $buildPage); - const prependTopDefault = 20; - - // header + navigation + margin - let topPostion = 168; - - if ($header.length) { - topPostion += $header.outerHeight(); - } - - if ($runnersStuck.length) { - topPostion += $runnersStuck.outerHeight(); - } - - if ($startsEnvironment.length) { - topPostion += $startsEnvironment.outerHeight() + prependTopDefault; - } - - if ($erased.length) { - topPostion += $erased.outerHeight() + prependTopDefault; - } - - if ($flashError.length) { - topPostion += $flashError.outerHeight(); - } - - this.$buildTrace.css({ - top: topPostion, - }); - }; - Build.prototype.initSidebar = function () { this.$sidebar = $('.js-build-sidebar'); this.$sidebar.niceScroll(); @@ -196,10 +173,13 @@ window.Build = (function () { }) .done((log) => { gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`); + if (log.state) { this.state = log.state; } + this.windowSize = this.$buildTraceOutput.prop('scrollHeight'); + if (log.append) { this.$buildTraceOutput.append(log.html); this.logBytes += log.size; @@ -220,16 +200,14 @@ window.Build = (function () { } if (!log.complete) { - this.toggleScrollAnimation(true); + if (!this.hasBeenScrolled) { + this.toggleScrollAnimation(true); + } else { + this.toggleScrollAnimation(false); + } Build.timeout = setTimeout(() => { - //eslint-disable-next-line - this.getBuildTrace() - .then(() => { - if (!this.hasBeenScrolled) { - this.scrollToBottom(); - } - }); + this.getBuildTrace(); }, 4000); } else { this.$buildRefreshAnimation.remove(); @@ -242,7 +220,13 @@ window.Build = (function () { }) .fail(() => { this.$buildRefreshAnimation.remove(); - }); + }) + .then(() => { + if (!this.hasBeenScrolled) { + this.scrollDown(); + } + }) + .then(() => this.toggleScroll()); }; Build.prototype.shouldHideSidebarForViewport = function () { @@ -254,14 +238,11 @@ window.Build = (function () { const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; const $toggleButton = $('.js-sidebar-build-toggle-header'); - this.$buildTrace - .toggleClass('sidebar-expanded', shouldShow) - .toggleClass('sidebar-collapsed', shouldHide); this.$sidebar .toggleClass('right-sidebar-expanded', shouldShow) .toggleClass('right-sidebar-collapsed', shouldHide); - $('.js-build-page') + this.$topBar .toggleClass('sidebar-expanded', shouldShow) .toggleClass('sidebar-collapsed', shouldHide); @@ -274,17 +255,10 @@ window.Build = (function () { Build.prototype.sidebarOnResize = function () { this.toggleSidebar(this.shouldHideSidebarForViewport()); - - this.verifyTopPosition(); - - if (this.canScroll()) { - this.toggleScroll(); - } }; Build.prototype.sidebarOnClick = function () { if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); - this.verifyTopPosition(); }; Build.prototype.updateArtifactRemoveDate = function () { diff --git a/app/assets/javascripts/close_reopen_report_toggle.js b/app/assets/javascripts/close_reopen_report_toggle.js new file mode 100644 index 00000000000..882d20671cc --- /dev/null +++ b/app/assets/javascripts/close_reopen_report_toggle.js @@ -0,0 +1,97 @@ +import DropLab from './droplab/drop_lab'; +import ISetter from './droplab/plugins/input_setter'; + +// Todo: Remove this when fixing issue in input_setter plugin +const InputSetter = Object.assign({}, ISetter); + +class CloseReopenReportToggle { + constructor(opts = {}) { + this.dropdownTrigger = opts.dropdownTrigger; + this.dropdownList = opts.dropdownList; + this.button = opts.button; + } + + initDroplab() { + this.reopenItem = this.dropdownList.querySelector('.reopen-item'); + this.closeItem = this.dropdownList.querySelector('.close-item'); + + this.droplab = new DropLab(); + + const config = this.setConfig(); + + this.droplab.init(this.dropdownTrigger, this.dropdownList, [InputSetter], config); + } + + updateButton(isClosed) { + this.toggleButtonType(isClosed); + + this.button.blur(); + } + + toggleButtonType(isClosed) { + const [showItem, hideItem] = this.getButtonTypes(isClosed); + + showItem.classList.remove('hidden'); + showItem.classList.add('droplab-item-selected'); + + hideItem.classList.add('hidden'); + hideItem.classList.remove('droplab-item-selected'); + + showItem.click(); + } + + getButtonTypes(isClosed) { + return isClosed ? [this.reopenItem, this.closeItem] : [this.closeItem, this.reopenItem]; + } + + setDisable(shouldDisable = true) { + if (shouldDisable) { + this.button.setAttribute('disabled', 'true'); + this.dropdownTrigger.setAttribute('disabled', 'true'); + } else { + this.button.removeAttribute('disabled'); + this.dropdownTrigger.removeAttribute('disabled'); + } + } + + setConfig() { + const config = { + InputSetter: [ + { + input: this.button, + valueAttribute: 'data-text', + inputAttribute: 'data-value', + }, + { + input: this.button, + valueAttribute: 'data-text', + inputAttribute: 'title', + }, + { + input: this.button, + valueAttribute: 'data-button-class', + inputAttribute: 'class', + }, + { + input: this.dropdownTrigger, + valueAttribute: 'data-toggle-class', + inputAttribute: 'class', + }, + { + input: this.button, + valueAttribute: 'data-url', + inputAttribute: 'href', + }, + { + input: this.button, + valueAttribute: 'data-method', + inputAttribute: 'data-method', + }, + ], + }; + + return config; + } +} + +export default CloseReopenReportToggle; diff --git a/app/assets/javascripts/comment_type_toggle.js b/app/assets/javascripts/comment_type_toggle.js index df0ba86198c..c74184949df 100644 --- a/app/assets/javascripts/comment_type_toggle.js +++ b/app/assets/javascripts/comment_type_toggle.js @@ -1,5 +1,8 @@ import DropLab from './droplab/drop_lab'; -import InputSetter from './droplab/plugins/input_setter'; +import ISetter from './droplab/plugins/input_setter'; + +// Todo: Remove this when fixing issue in input_setter plugin +const InputSetter = Object.assign({}, ISetter); class CommentTypeToggle { constructor(opts = {}) { diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index 2c38440a2af..687f09882a7 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -18,13 +18,26 @@ window.gl.CommitPipelinesTable = CommitPipelinesTable; document.addEventListener('DOMContentLoaded', () => { const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { - const table = new CommitPipelinesTable({ - propsData: { - endpoint: pipelineTableViewEl.dataset.endpoint, - helpPagePath: pipelineTableViewEl.dataset.helpPagePath, - }, - }).$mount(); - pipelineTableViewEl.appendChild(table.$el); + if (pipelineTableViewEl) { + // Update MR and Commits tabs + pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => { + if (event.detail.pipelines && + event.detail.pipelines.count && + event.detail.pipelines.count.all) { + const badge = document.querySelector('.js-pipelines-mr-count'); + + badge.textContent = event.detail.pipelines.count.all; + } + }); + + if (pipelineTableViewEl.dataset.disableInitialization === undefined) { + const table = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + }, + }).$mount(); + pipelineTableViewEl.appendChild(table.$el); + } } }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 3c77f14d533..dd751ec97a8 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -51,11 +51,22 @@ }, methods: { successCallback(resp) { - const response = resp.json(); + return resp.json().then((response) => { + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = response.pipelines || response; + this.setCommonData(pipelines); - // depending of the endpoint the response can either bring a `pipelines` key or not. - const pipelines = response.pipelines || response; - this.setCommonData(pipelines); + const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { + detail: { + pipelines: response, + }, + }); + + // notifiy to update the count in tabs + if (this.$el.parentElement) { + this.$el.parentElement.dispatchEvent(updatePipelinesEvent); + } + }); }, }, }; diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js index 36bfe457be9..510bedbf641 100644 --- a/app/assets/javascripts/commons/bootstrap.js +++ b/app/assets/javascripts/commons/bootstrap.js @@ -8,6 +8,7 @@ import 'bootstrap-sass/assets/javascripts/bootstrap/modal'; import 'bootstrap-sass/assets/javascripts/bootstrap/tab'; import 'bootstrap-sass/assets/javascripts/bootstrap/transition'; import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip'; +import 'bootstrap-sass/assets/javascripts/bootstrap/popover'; // custom jQuery functions $.fn.extend({ diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index ba9d9a3e1f7..54257531284 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -1,6 +1,7 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ import './lib/utils/common_utils'; +import { placeholderImage } from './lazy_loader'; const gfmRules = { // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert @@ -56,6 +57,11 @@ const gfmRules = { return text; }, }, + ImageLazyLoadFilter: { + 'img'(el, text) { + return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; + }, + }, VideoLinkFilter: { '.video-container'(el) { const videoEl = el.querySelector('video'); @@ -163,7 +169,9 @@ const gfmRules = { return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); }, 'img'(el) { - return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; + const imageSrc = el.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 diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 725ec7b9c70..6a008112203 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,6 +1,8 @@ /* eslint-disable class-methods-use-this */ import './lib/utils/url_utility'; +import FilesCommentButton from './files_comment_button'; +import SingleFileDiff from './single_file_diff'; const UNFOLD_COUNT = 20; let isBound = false; @@ -8,8 +10,14 @@ let isBound = false; class Diff { constructor() { const $diffFile = $('.files .diff-file'); - $diffFile.singleFileDiff(); - $diffFile.filesCommentButton(); + + $diffFile.each((index, file) => { + if (!$.data(file, 'singleFileDiff')) { + $.data(file, 'singleFileDiff', new SingleFileDiff(file)); + } + }); + + FilesCommentButton.init($diffFile); $diffFile.each((index, file) => new gl.ImageFile(file)); 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 517bdb6be09..c37249c060a 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -139,9 +139,9 @@ const DiffNoteAvatars = Vue.extend({ const notesCount = this.notesCount; $(this.$el).closest('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0) + .toggleClass('no-comment-btn', notesCount > 0) .nextUntil('.js-avatar-container') - .toggleClass('js-no-comment-btn', notesCount > 0); + .toggleClass('no-comment-btn', notesCount > 0); }, toggleDiscussionsToggleState() { const $notesHolders = $(this.$el).closest('.code').find('.notes_holder'); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index 9d51fb53eb2..efb6ced9f46 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */ +/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, max-len */ /* global CommentsStore */ /* global ResolveService */ /* global Flash */ @@ -64,8 +64,6 @@ const ResolveBtn = Vue.extend({ }); }, resolve: function () { - const errorFlashMsg = 'An error occurred when trying to resolve a comment. Please try again.'; - if (!this.canResolve) return; let promise; @@ -79,24 +77,20 @@ const ResolveBtn = Vue.extend({ .resolve(this.noteId); } - promise.then((response) => { - this.loading = false; + promise + .then(resp => resp.json()) + .then((data) => { + this.loading = false; - if (response.status === 200) { - const data = response.json(); const resolved_by = data ? data.resolved_by : null; CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by); this.discussion.updateHeadline(data); gl.mrWidget.checkStatus(); - } else { - new Flash(errorFlashMsg); - } - this.updateTooltip(); - }).catch(() => { - new Flash(errorFlashMsg); - }); + this.updateTooltip(); + }) + .catch(() => new Flash('An error occurred when trying to resolve a comment. Please try again.')); } }, mounted: function () { diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js index 807ab11d292..2f063f6fe1f 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -1,4 +1,3 @@ -/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */ /* global Flash */ /* global CommentsStore */ @@ -32,27 +31,22 @@ class ResolveServiceClass { promise = this.resolveAll(mergeRequestId, discussionId); } - promise.then((response) => { - discussion.loading = false; - - if (response.status === 200) { - const data = response.json(); - const resolved_by = data ? data.resolved_by : null; + promise + .then(resp => resp.json()) + .then((data) => { + discussion.loading = false; + const resolvedBy = data ? data.resolved_by : null; if (isResolved) { discussion.unResolveAllNotes(); } else { - discussion.resolveAllNotes(resolved_by); + discussion.resolveAllNotes(resolvedBy); } gl.mrWidget.checkStatus(); discussion.updateHeadline(data); - } else { - throw new Error('An error occurred when trying to resolve discussion.'); - } - }).catch(() => { - new Flash('An error occurred when trying to resolve a discussion. Please try again.'); - }); + }) + .catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.')); } resolveAll(mergeRequestId, discussionId) { @@ -62,7 +56,7 @@ class ResolveServiceClass { return this.discussionResource.save({ mergeRequestId, - discussionId + discussionId, }, {}); } @@ -73,7 +67,7 @@ class ResolveServiceClass { return this.discussionResource.delete({ mergeRequestId, - discussionId + discussionId, }, {}); } } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 88b4b567fa9..d5923266c60 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,17 +1,15 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ -/* global UsernameValidator */ -/* global ActiveTabMemoizer */ +/* global ProjectSelect */ /* global ShortcutsNavigation */ /* global IssuableIndex */ /* global ShortcutsIssuable */ -/* global ZenMode */ /* global Milestone */ /* global IssuableForm */ /* global LabelsSelect */ /* global MilestoneSelect */ /* global Commit */ +/* global NewBranchForm */ /* global NotificationsForm */ -/* global TreeView */ /* global NotificationsDropdown */ /* global GroupAvatar */ /* global LineHighlighter */ @@ -23,9 +21,10 @@ /* global NamespaceSelects */ /* global Project */ /* global ProjectAvatar */ +/* global MergeRequest */ +/* global Compare */ /* global CompareAutocomplete */ /* global ProjectNew */ -/* global Star */ /* global ProjectShow */ /* global Labels */ /* global Shortcuts */ @@ -45,7 +44,6 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; import Landing from './landing'; import BlobForkSuggestion from './blob/blob_fork_suggestion'; import UserCallout from './user_callout'; -import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; import ShortcutsWiki from './shortcuts_wiki'; import Pipelines from './pipelines'; import BlobViewer from './blob/viewer/index'; @@ -54,7 +52,23 @@ import UsersSelect from './users_select'; import RefSelectDropdown from './ref_select_dropdown'; import GfmAutoComplete from './gfm_auto_complete'; import ShortcutsBlob from './shortcuts_blob'; +import SigninTabsMemoizer from './signin_tabs_memoizer'; +import Star from './star'; +import Todos from './todos'; +import TreeView from './tree'; +import UsagePing from './usage_ping'; +import UsernameValidator from './username_validator'; +import VersionCheckImage from './version_check_image'; +import Wikis from './wikis'; +import ZenMode from './zen_mode'; import initSettingsPanels from './settings_panels'; +import initExperimentalFlags from './experimental_flags'; +import OAuthRememberMe from './oauth_remember_me'; +import PerformanceBar from './performance_bar'; +import initNotes from './init_notes'; +import initLegacyFilters from './init_legacy_filters'; +import initIssuableSidebar from './init_issuable_sidebar'; +import GpgBadges from './gpg_badges'; (function() { var Dispatcher; @@ -120,9 +134,13 @@ import initSettingsPanels from './settings_panels'; } switch (page) { + case 'profiles:preferences:show': + initExperimentalFlags(); + break; case 'sessions:new': new UsernameValidator(); - new ActiveTabMemoizer(); + new SigninTabsMemoizer(); + new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents(); break; case 'projects:boards:show': case 'projects:boards:index': @@ -145,6 +163,11 @@ import initSettingsPanels from './settings_panels'; new Issue(); shortcut_handler = new ShortcutsIssuable(); new ZenMode(); + initIssuableSidebar(); + initNotes(); + break; + case 'dashboard:milestones:index': + new ProjectSelect(); break; case 'projects:milestones:show': case 'groups:milestones:show': @@ -152,12 +175,15 @@ import initSettingsPanels from './settings_panels'; new Milestone(); new Sidebar(); break; + case 'dashboard:issues': + case 'dashboard:merge_requests': case 'groups:issues': case 'groups:merge_requests': - new UsersSelect(); + new ProjectSelect(); + initLegacyFilters(); break; case 'dashboard:todos:index': - new gl.Todos(); + new Todos(); break; case 'dashboard:projects:index': case 'dashboard:projects:starred': @@ -205,8 +231,21 @@ import initSettingsPanels from './settings_panels'; new MilestoneSelect(); new gl.IssuableTemplateSelectors(); break; - case 'projects:merge_requests:new': - case 'projects:merge_requests:new_diffs': + case 'projects:merge_requests:creations:new': + const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); + if (mrNewCompareNode) { + new Compare({ + targetProjectUrl: mrNewCompareNode.dataset.targetProjectUrl, + sourceBranchUrl: mrNewCompareNode.dataset.sourceBranchUrl, + targetBranchUrl: mrNewCompareNode.dataset.targetBranchUrl, + }); + } else { + const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit'); + new MergeRequest({ + action: mrNewSubmitNode.dataset.mrSubmitAction, + }); + } + case 'projects:merge_requests:creations:diffs': case 'projects:merge_requests:edit': new gl.Diff(); shortcut_handler = new ShortcutsNavigation(); @@ -220,7 +259,10 @@ import initSettingsPanels from './settings_panels'; case 'projects:tags:new': new ZenMode(); new gl.GLForm($('.tag-form'), true); - new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs); + new RefSelectDropdown($('.js-branch-select')); + break; + case 'projects:snippets:show': + initNotes(); break; case 'projects:snippets:new': case 'projects:snippets:edit': @@ -242,18 +284,18 @@ import initSettingsPanels from './settings_panels'; new gl.Diff(); shortcut_handler = new ShortcutsIssuable(true); new ZenMode(); - break; - case "projects:merge_requests:diffs": - new gl.Diff(); - new ZenMode(); + + initIssuableSidebar(); + initNotes(); + + const mrShowNode = document.querySelector('.merge-request'); + window.mergeRequest = new MergeRequest({ + action: mrShowNode.dataset.mrAction, + }); break; case 'dashboard:activity': new gl.Activities(); break; - case 'dashboard:issues': - case 'dashboard:merge_requests': - new UsersSelect(); - break; case 'projects:commit:show': new Commit(); new gl.Diff(); @@ -262,6 +304,7 @@ import initSettingsPanels from './settings_panels'; new MiniPipelineGraph({ container: '.js-commit-pipeline-graph', }).bindEvents(); + initNotes(); break; case 'projects:commit:pipelines': new MiniPipelineGraph({ @@ -269,6 +312,9 @@ import initSettingsPanels from './settings_panels'; }).bindEvents(); break; case 'projects:commits:show': + shortcut_handler = new ShortcutsNavigation(); + GpgBadges.fetch(); + break; case 'projects:activity': shortcut_handler = new ShortcutsNavigation(); break; @@ -285,6 +331,9 @@ import initSettingsPanels from './settings_panels'; case 'projects:edit': setupProjectEdit(); break; + case 'projects:pipelines:new': + new NewBranchForm($('.js-new-pipeline-form')); + break; case 'projects:pipelines:builds': case 'projects:pipelines:failures': case 'projects:pipelines:show': @@ -315,7 +364,7 @@ import initSettingsPanels from './settings_panels'; new gl.Members(); new UsersSelect(); break; - case 'projects:members:show': + case 'projects:project_members:index': new gl.MemberExpirationDate('.js-access-expiration-date-groups'); new GroupsSelect(); new gl.MemberExpirationDate(); @@ -338,6 +387,9 @@ import initSettingsPanels from './settings_panels'; shortcut_handler = new ShortcutsNavigation(); new TreeView(); new BlobViewer(); + $('#tree-slider').waitForImages(function() { + gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); + }); break; case 'projects:find_file:show': shortcut_handler = true; @@ -355,10 +407,20 @@ import initSettingsPanels from './settings_panels'; case 'projects:labels:edit': new Labels(); break; + case 'groups:labels:index': case 'projects:labels:index': if ($('.prioritized-labels').length) { new gl.LabelManager(); } + $('.label-subscription').each((i, el) => { + const $el = $(el); + + if ($el.find('.dropdown-group-label').length) { + new gl.GroupLabelSubscription($el); + } else { + new gl.ProjectLabelSubscription($el); + } + }); break; case 'projects:network:show': // Ensure we don't create a particular shortcut handler here. This is @@ -377,22 +439,17 @@ import initSettingsPanels from './settings_panels'; new BlobViewer(); break; case 'help:index': - gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); + VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); break; case 'search:show': new Search(); break; - case 'projects:repository:show': - // Initialize Protected Branch Settings - new gl.ProtectedBranchCreate(); - new gl.ProtectedBranchEditList(); - // Initialize Protected Tag Settings - new ProtectedTagCreate(); - new ProtectedTagEditList(); + case 'projects:settings:repository:show': // Initialize expandable settings panels initSettingsPanels(); break; - case 'projects:ci_cd:show': + case 'projects:settings:ci_cd:show': + case 'groups:settings:ci_cd:show': new gl.ProjectVariables(); break; case 'ci:lints:create': @@ -408,10 +465,15 @@ import initSettingsPanels from './settings_panels'; case 'snippets:show': new LineHighlighter(); new BlobViewer(); + initNotes(); break; case 'import:fogbugz:new_user_map': new UsersSelect(); break; + case 'profiles:personal_access_tokens:index': + case 'admin:impersonation_tokens:index': + new gl.DueDateSelectors(); + break; } switch (path.first()) { case 'sessions': @@ -429,7 +491,7 @@ import initSettingsPanels from './settings_panels'; new Admin(); switch (path[1]) { case 'cohorts': - new gl.UsagePing(); + new UsagePing(); break; case 'groups': new UsersSelect(); @@ -481,10 +543,11 @@ import initSettingsPanels from './settings_panels'; new NotificationsDropdown(); break; case 'wikis': - new gl.Wikis(); + new Wikis(); shortcut_handler = new ShortcutsWiki(); new ZenMode(); new gl.GLForm($('.wiki-form'), true); + new Sidebar(); break; case 'snippets': shortcut_handler = new ShortcutsNavigation(); @@ -508,11 +571,22 @@ import initSettingsPanels from './settings_panels'; case 'protected_branches': shortcut_handler = new ShortcutsNavigation(); } + break; + case 'users': + const action = path[1]; + import(/* webpackChunkName: 'user_profile' */ './users') + .then(user => user.default(action)) + .catch(() => {}); + break; } // If we haven't installed a custom shortcut handler, install the default one if (!shortcut_handler) { new Shortcuts(); } + + if (document.querySelector('#peek')) { + new PerformanceBar({ container: '#peek' }); + } }; Dispatcher.prototype.initSearch = function() { diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 73675d300be..9ebbb22e807 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -5,21 +5,28 @@ import './preview_markdown'; window.DropzoneInput = (function() { function DropzoneInput(form) { - var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile, addFileToForm; Dropzone.autoDiscover = false; - divHover = '<div class="div-dropzone-hover"></div>'; - iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; - $attachButton = form.find('.button-attach-file'); - $attachingFileMessage = form.find('.attaching-file-message'); - $cancelButton = form.find('.button-cancel-uploading-files'); - $retryLink = form.find('.retry-uploading-link'); - $uploadProgress = form.find('.uploading-progress'); - $uploadingErrorContainer = form.find('.uploading-error-container'); - $uploadingErrorMessage = form.find('.uploading-error-message'); - $uploadingProgressContainer = form.find('.uploading-progress-container'); - uploadsPath = window.uploads_path || null; - maxFileSize = gon.max_file_size || 10; - formTextarea = form.find('.js-gfm-input'); + const divHover = '<div class="div-dropzone-hover"></div>'; + const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; + const $attachButton = form.find('.button-attach-file'); + const $attachingFileMessage = form.find('.attaching-file-message'); + const $cancelButton = form.find('.button-cancel-uploading-files'); + const $retryLink = form.find('.retry-uploading-link'); + const $uploadProgress = form.find('.uploading-progress'); + const $uploadingErrorContainer = form.find('.uploading-error-container'); + const $uploadingErrorMessage = form.find('.uploading-error-message'); + const $uploadingProgressContainer = form.find('.uploading-progress-container'); + const uploadsPath = window.uploads_path || null; + const maxFileSize = gon.max_file_size || 10; + const formTextarea = form.find('.js-gfm-input'); + let handlePaste; + let pasteText; + let addFileToForm; + let updateAttachingMessage; + let isImage; + let getFilename; + let uploadFile; + formTextarea.wrap('<div class="div-dropzone"></div>'); formTextarea.on('paste', (function(_this) { return function(event) { @@ -28,16 +35,16 @@ window.DropzoneInput = (function() { })(this)); // Add dropzone area to the form. - $mdArea = formTextarea.closest('.md-area'); + const $mdArea = formTextarea.closest('.md-area'); form.setupMarkdownPreview(); - $formDropzone = form.find('.div-dropzone'); + const $formDropzone = form.find('.div-dropzone'); $formDropzone.parent().addClass('div-dropzone-wrapper'); $formDropzone.append(divHover); $formDropzone.find('.div-dropzone-hover').append(iconPaperclip); if (!uploadsPath) return; - dropzone = $formDropzone.dropzone({ + const dropzone = $formDropzone.dropzone({ url: uploadsPath, dictDefaultMessage: '', clickable: true, @@ -117,7 +124,7 @@ window.DropzoneInput = (function() { } }); - child = $(dropzone[0]).children('textarea'); + const child = $(dropzone[0]).children('textarea'); // removeAllFiles(true) stops uploading files (if any) // and remove them from dropzone files queue. @@ -214,6 +221,35 @@ window.DropzoneInput = (function() { return value.first(); }; + const showSpinner = function(e) { + return $uploadingProgressContainer.removeClass('hide'); + }; + + const closeSpinner = function() { + return $uploadingProgressContainer.addClass('hide'); + }; + + const showError = function(message) { + $uploadingErrorContainer.removeClass('hide'); + $uploadingErrorMessage.html(message); + }; + + const closeAlertMessage = function() { + return form.find('.div-dropzone-alert').alert('close'); + }; + + const insertToTextArea = function(filename, url) { + return $(child).val(function(index, val) { + return val.replace(`{{${filename}}}`, url); + }); + }; + + const appendToTextArea = function(url) { + return $(child).val(function(index, val) { + return val + url + "\n"; + }); + }; + uploadFile = function(item, filename) { var formData; formData = new FormData(); @@ -262,35 +298,6 @@ window.DropzoneInput = (function() { messageContainer.text(attachingMessage); }; - insertToTextArea = function(filename, url) { - return $(child).val(function(index, val) { - return val.replace(`{{${filename}}}`, url); - }); - }; - - appendToTextArea = function(url) { - return $(child).val(function(index, val) { - return val + url + "\n"; - }); - }; - - showSpinner = function(e) { - return $uploadingProgressContainer.removeClass('hide'); - }; - - closeSpinner = function() { - return $uploadingProgressContainer.addClass('hide'); - }; - - showError = function(message) { - $uploadingErrorContainer.removeClass('hide'); - $uploadingErrorMessage.html(message); - }; - - closeAlertMessage = function() { - return form.find('.div-dropzone-alert').alert('close'); - }; - form.find('.markdown-selector').click(function(e) { e.preventDefault(); $(this).closest('.gfm-form').find('.div-dropzone').click(); diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js index a8fc5b41fb4..2856c8e2862 100644 --- a/app/assets/javascripts/due_date_select.js +++ b/app/assets/javascripts/due_date_select.js @@ -2,6 +2,8 @@ /* global dateFormat */ /* global Pikaday */ +import DateFix from './lib/utils/datefix'; + class DueDateSelect { constructor({ $dropdown, $loading } = {}) { const $dropdownParent = $dropdown.closest('.dropdown'); @@ -43,14 +45,13 @@ class DueDateSelect { initDatePicker() { const $dueDateInput = $(`input[name='${this.fieldName}']`); - + const dateFix = DateFix.dashedFix($dueDateInput.val()); const calendar = new Pikaday({ field: $dueDateInput.get(0), theme: 'gitlab-theme', format: 'yyyy-mm-dd', onSelect: (dateText) => { const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); - $dueDateInput.val(formattedDate); if (this.$dropdown.hasClass('js-issue-boards-due-date')) { @@ -62,7 +63,7 @@ class DueDateSelect { } }); - calendar.setDate(new Date($dueDateInput.val())); + calendar.setDate(dateFix); this.$datePicker.append(calendar.el); this.$datePicker.data('pikaday', calendar); } @@ -168,6 +169,7 @@ class DueDateSelectors { initMilestoneDatePicker() { $('.datepicker').each(function() { const $datePicker = $(this); + const dateFix = DateFix.dashedFix($datePicker.val()); const calendar = new Pikaday({ field: $datePicker.get(0), theme: 'gitlab-theme animate-picker', @@ -177,7 +179,8 @@ class DueDateSelectors { $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); } }); - calendar.setDate(new Date($datePicker.val())); + + calendar.setDate(dateFix); $datePicker.data('pikaday', calendar); }); diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js new file mode 100644 index 00000000000..cac35d6eed5 --- /dev/null +++ b/app/assets/javascripts/emoji/index.js @@ -0,0 +1,99 @@ +import emojiMap from 'emojis/digests.json'; +import emojiAliases from 'emojis/aliases.json'; + +export const validEmojiNames = [...Object.keys(emojiMap), ...Object.keys(emojiAliases)]; + +export function normalizeEmojiName(name) { + return Object.prototype.hasOwnProperty.call(emojiAliases, name) ? emojiAliases[name] : name; +} + +export function isEmojiNameValid(name) { + return validEmojiNames.indexOf(name) >= 0; +} + +export function filterEmojiNames(filter) { + const match = filter.toLowerCase(); + return validEmojiNames.filter(name => name.indexOf(match) >= 0); +} + +export function filterEmojiNamesByAlias(filter) { + return _.uniq(filterEmojiNames(filter).map(name => normalizeEmojiName(name))); +} + +let emojiCategoryMap; +export function getEmojiCategoryMap() { + if (!emojiCategoryMap) { + emojiCategoryMap = { + activity: [], + people: [], + nature: [], + food: [], + travel: [], + objects: [], + symbols: [], + flags: [], + }; + Object.keys(emojiMap).forEach((name) => { + const emoji = emojiMap[name]; + if (emojiCategoryMap[emoji.category]) { + emojiCategoryMap[emoji.category].push(name); + } + }); + } + return emojiCategoryMap; +} + +export function getEmojiInfo(query) { + let name = normalizeEmojiName(query); + let emojiInfo = emojiMap[name]; + + // Fallback to question mark for unknown emojis + if (!emojiInfo) { + name = 'grey_question'; + emojiInfo = emojiMap[name]; + } + + return { ...emojiInfo, name }; +} + +export function emojiFallbackImageSrc(inputName) { + const { name, digest } = getEmojiInfo(inputName); + return `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${digest}.png`; +} + +export function emojiImageTag(name, src) { + return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`; +} + +export function glEmojiTag(inputName, options) { + const opts = { sprite: false, forceFallback: false, ...options }; + const { name, ...emojiInfo } = getEmojiInfo(inputName); + + const fallbackImageSrc = emojiFallbackImageSrc(name); + const fallbackSpriteClass = `emoji-${name}`; + + const classList = []; + if (opts.forceFallback && opts.sprite) { + classList.push('emoji-icon'); + classList.push(fallbackSpriteClass); + } + const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : ''; + const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : ''; + let contents = emojiInfo.moji; + if (opts.forceFallback && !opts.sprite) { + contents = emojiImageTag(name, fallbackImageSrc); + } + + return ` + <gl-emoji + ${classAttribute} + data-name="${name}" + data-fallback-src="${fallbackImageSrc}" + ${fallbackSpriteAttribute} + data-unicode-version="${emojiInfo.unicodeVersion}" + title="${emojiInfo.description}" + > + ${contents} + </gl-emoji> + `; +} diff --git a/app/assets/javascripts/emoji/support/index.js b/app/assets/javascripts/emoji/support/index.js new file mode 100644 index 00000000000..1f7852dd487 --- /dev/null +++ b/app/assets/javascripts/emoji/support/index.js @@ -0,0 +1,10 @@ +import isEmojiUnicodeSupported from './is_emoji_unicode_supported'; +import getUnicodeSupportMap from './unicode_support_map'; + +// cache browser support map between calls +let browserUnicodeSupportMap; + +export default function isEmojiUnicodeSupportedByBrowser(emojiUnicode, unicodeVersion) { + browserUnicodeSupportMap = browserUnicodeSupportMap || getUnicodeSupportMap(); + return isEmojiUnicodeSupported(browserUnicodeSupportMap, emojiUnicode, unicodeVersion); +} diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js index 4f8884d05ac..3fd23efa9f8 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js @@ -111,7 +111,7 @@ function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVe } export { - isEmojiUnicodeSupported, + isEmojiUnicodeSupported as default, isFlagEmoji, isKeycapEmoji, isSkinToneComboEmoji, diff --git a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js b/app/assets/javascripts/emoji/support/unicode_support_map.js index 257df55e54f..755381c2f95 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/unicode_support_map.js +++ b/app/assets/javascripts/emoji/support/unicode_support_map.js @@ -140,7 +140,7 @@ function generateUnicodeSupportMap(testMap) { return resultMap; } -function getUnicodeSupportMap() { +export default function getUnicodeSupportMap() { let unicodeSupportMap; let userAgentFromCache; @@ -165,8 +165,3 @@ function getUnicodeSupportMap() { return unicodeSupportMap; } - -export { - getUnicodeSupportMap, - generateUnicodeSupportMap, -}; diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue index 8120ef182d4..91ed8c8467f 100644 --- a/app/assets/javascripts/environments/components/environment.vue +++ b/app/assets/javascripts/environments/components/environment.vue @@ -32,7 +32,6 @@ export default { state: store.state, visibility: 'available', isLoading: false, - isLoadingFolderContent: false, cssContainerClass: environmentsData.cssClass, endpoint: environmentsData.environmentsDataEndpoint, canCreateDeployment: environmentsData.canCreateDeployment, @@ -86,9 +85,6 @@ export default { errorCallback: this.errorCallback, notificationCallback: (isMakingRequest) => { this.isMakingRequest = isMakingRequest; - - // We need to verify if any folder is open to also fecth it - this.openFolders = this.store.getOpenFolders(); }, }); @@ -119,7 +115,7 @@ export default { this.store.toggleFolder(folder); if (!folder.isOpen) { - this.fetchChildEnvironments(folder, folderUrl); + this.fetchChildEnvironments(folder, folderUrl, true); } }, @@ -147,19 +143,17 @@ export default { .catch(this.errorCallback); }, - fetchChildEnvironments(folder, folderUrl) { - this.isLoadingFolderContent = true; + fetchChildEnvironments(folder, folderUrl, showLoader = false) { + this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader); this.service.getFolderContent(folderUrl) .then(resp => resp.json()) - .then((response) => { - this.store.setfolderContent(folder, response.environments); - this.isLoadingFolderContent = false; - }) + .then(response => this.store.setfolderContent(folder, response.environments)) + .then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false)) .catch(() => { - this.isLoadingFolderContent = false; // eslint-disable-next-line no-new new Flash('An error occurred while fetching the environments.'); + this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false); }); }, @@ -176,13 +170,13 @@ export default { successCallback(resp) { this.saveData(resp); - // If folders are open while polling we need to open them again - if (this.openFolders.length) { - this.openFolders.map((folder) => { + // We need to verify if any folder is open to also update it + const openFolders = this.store.getOpenFolders(); + if (openFolders.length) { + openFolders.forEach((folder) => { // TODO - Move this to the backend const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`; - this.store.updateFolder(folder, 'isOpen', true); return this.fetchChildEnvironments(folder, folderUrl); }); } @@ -267,7 +261,7 @@ export default { :environments="state.environments" :can-create-deployment="canCreateDeploymentParsed" :can-read-environment="canReadEnvironmentParsed" - :is-loading-folder-content="isLoadingFolderContent" /> + /> </div> <table-pagination diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index a2448520a5f..e7495677e7c 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -2,6 +2,7 @@ import playIconSvg from 'icons/_icon_play.svg'; import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -12,6 +13,10 @@ export default { }, }, + directives: { + tooltip, + }, + components: { loadingIcon, }, @@ -33,8 +38,6 @@ export default { onClickAction(endpoint) { this.isLoading = true; - $(this.$refs.tooltip).tooltip('destroy'); - eventHub.$emit('postAction', endpoint); }, @@ -53,11 +56,11 @@ export default { class="btn-group" role="group"> <button + v-tooltip type="button" - class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip" + class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" data-container="body" data-toggle="dropdown" - ref="tooltip" :title="title" :aria-label="title" :disabled="isLoading"> diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue index eaeec2bc53c..6b749814ea4 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.vue +++ b/app/assets/javascripts/environments/components/environment_external_url.vue @@ -1,4 +1,6 @@ <script> +import tooltip from '../../vue_shared/directives/tooltip'; + /** * Renders the external url link in environments table. */ @@ -10,6 +12,10 @@ export default { }, }, + directives: { + tooltip, + }, + computed: { title() { return 'Open'; @@ -19,7 +25,8 @@ export default { </script> <template> <a - class="btn external-url has-tooltip" + v-tooltip + class="btn external-url" data-container="body" target="_blank" rel="noopener noreferrer nofollow" diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 809c147bf25..d8b1b2f1b92 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -403,6 +403,14 @@ export default { return ''; }, + displayEnvironmentActions() { + return this.hasManualActions || + this.externalURL || + this.monitoringUrl || + this.hasStopAction || + this.canRetry; + }, + /** * Constructs folder URL based on the current location and the folder id. * @@ -490,9 +498,9 @@ export default { <div class="table-section section-15 hidden-xs hidden-sm" role="gridcell"> <a v-if="shouldRenderBuildName" - class="build-link" + class="build-link flex-truncate-parent" :href="buildPath"> - {{buildName}} + <span class="flex-truncate-child">{{buildName}}</span> </a> </div> @@ -535,9 +543,12 @@ export default { </span> </div> - <div class="table-section section-30 table-button-footer" role="gridcell"> + <div + v-if="!model.isFolder && displayEnvironmentActions" + class="table-section section-30 table-button-footer" + role="gridcell"> + <div - v-if="!model.isFolder" class="btn-group table-action-buttons" role="group"> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index 07cf92281a0..1655561cdd3 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -2,6 +2,8 @@ /** * Renders the Monitoring (Metrics) link in environments table. */ +import tooltip from '../../vue_shared/directives/tooltip'; + export default { props: { monitoringUrl: { @@ -10,6 +12,10 @@ export default { }, }, + directives: { + tooltip, + }, + computed: { title() { return 'Monitoring'; @@ -19,7 +25,8 @@ export default { </script> <template> <a - class="btn monitoring-url has-tooltip hidden-xs hidden-sm" + v-tooltip + class="btn monitoring-url hidden-xs hidden-sm" data-container="body" rel="noopener noreferrer nofollow" :href="monitoringUrl" diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index 091c543860b..85f11d2071b 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -5,6 +5,7 @@ */ import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -14,6 +15,10 @@ export default { }, }, + directives: { + tooltip, + }, + data() { return { isLoading: false, @@ -46,8 +51,9 @@ export default { </script> <template> <button + v-tooltip type="button" - class="btn stop-env-link has-tooltip hidden-xs hidden-sm" + class="btn stop-env-link hidden-xs hidden-sm" data-container="body" @click="onClick" :disabled="isLoading" diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 1ca65a79951..2037bf618e3 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -4,6 +4,7 @@ * Used in environments table. */ import terminalIconSvg from 'icons/_icon_terminal.svg'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -14,6 +15,10 @@ export default { }, }, + directives: { + tooltip, + }, + data() { return { terminalIconSvg, @@ -29,7 +34,8 @@ export default { </script> <template> <a - class="btn terminal-button has-tooltip hidden-xs hidden-sm" + v-tooltip + class="btn terminal-button hidden-xs hidden-sm" data-container="body" :title="title" :aria-label="title" diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index b1fd9db650b..175cc8f1f72 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -29,12 +29,6 @@ export default { required: false, default: false, }, - - isLoadingFolderContent: { - type: Boolean, - required: false, - default: false, - }, }, methods: { @@ -74,7 +68,7 @@ export default { /> <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> - <div v-if="isLoadingFolderContent"> + <div v-if="model.isLoadingFolderContent"> <loading-icon size="2" /> </div> diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 25b24fbd6dc..8f4066e3a6e 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -1,17 +1,15 @@ export default { methods: { saveData(resp) { - const response = { - headers: resp.headers, - body: resp.json(), - }; + const headers = resp.headers; + return resp.json().then((response) => { + this.isLoading = false; - this.isLoading = false; - - this.store.storeAvailableCount(response.body.available_count); - this.store.storeStoppedCount(response.body.stopped_count); - this.store.storeEnvironments(response.body.environments); - this.store.setPagination(response.headers); + this.store.storeAvailableCount(response.available_count); + this.store.storeStoppedCount(response.stopped_count); + this.store.storeEnvironments(response.environments); + this.store.setPagination(headers); + }); }, }, }; diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index a5773dd7e4f..038c149be2d 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -35,14 +35,18 @@ export default class EnvironmentsStore { */ storeEnvironments(environments = []) { const filteredEnvironments = environments.map((env) => { + const oldEnvironmentState = this.state.environments + .find(element => element.id === env.latest.id) || {}; + let filtered = {}; if (env.size > 1) { filtered = Object.assign({}, env, { isFolder: true, + isLoadingFolderContent: oldEnvironmentState.isLoading || false, folderName: env.name, - isOpen: false, - children: [], + isOpen: oldEnvironmentState.isOpen || false, + children: oldEnvironmentState.children || [], }); } @@ -98,7 +102,7 @@ export default class EnvironmentsStore { * @return {Array} */ toggleFolder(folder) { - return this.updateFolder(folder, 'isOpen', !folder.isOpen); + return this.updateEnvironmentProp(folder, 'isOpen', !folder.isOpen); } /** @@ -125,23 +129,23 @@ export default class EnvironmentsStore { return updated; }); - return this.updateFolder(folder, 'children', updatedEnvironments); + return this.updateEnvironmentProp(folder, 'children', updatedEnvironments); } /** - * Given a folder a prop and a new value updates the correct folder. + * Given a environment, a prop and a new value updates the correct environment. * - * @param {Object} folder + * @param {Object} environment * @param {String} prop * @param {String|Boolean|Object|Array} newValue * @return {Array} */ - updateFolder(folder, prop, newValue) { + updateEnvironmentProp(environment, prop, newValue) { const environments = this.state.environments; const updatedEnvironments = environments.map((env) => { const updateEnv = Object.assign({}, env); - if (env.isFolder && env.id === folder.id) { + if (env.id === environment.id) { updateEnv[prop] = newValue; } @@ -149,8 +153,6 @@ export default class EnvironmentsStore { }); this.state.environments = updatedEnvironments; - - return updatedEnvironments; } getOpenFolders() { diff --git a/app/assets/javascripts/experimental_flags.js b/app/assets/javascripts/experimental_flags.js new file mode 100644 index 00000000000..6ee65ca72f9 --- /dev/null +++ b/app/assets/javascripts/experimental_flags.js @@ -0,0 +1,14 @@ +import Cookies from 'js-cookie'; + +export default () => { + $('.js-experiment-feature-toggle').on('change', (e) => { + const el = e.target; + + Cookies.set(el.name, el.value, { + expires: 365 * 10, + }); + + document.body.scrollTop = 0; + window.location.reload(); + }); +}; diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 534e651b030..d02e4cd5876 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,150 +1,73 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ -/* global FilesCommentButton */ /* global notes */ -let $commentButtonTemplate; - -window.FilesCommentButton = (function() { - var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; - - COMMENT_BUTTON_CLASS = '.add-diff-note'; - - LINE_HOLDER_CLASS = '.line_holder'; - - LINE_NUMBER_CLASS = 'diff-line-num'; - - LINE_CONTENT_CLASS = 'line_content'; - - UNFOLDABLE_LINE_CLASS = 'js-unfold'; - - EMPTY_CELL_CLASS = 'empty-cell'; - - OLD_LINE_CLASS = 'old_line'; - - LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content"; - - TEXT_FILE_SELECTOR = '.text-file'; - - function FilesCommentButton(filesContainerElement) { - this.render = this.render.bind(this); - this.hideButton = this.hideButton.bind(this); - this.isParallelView = notes.isParallelView(); - filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render) - .on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton); - } - - FilesCommentButton.prototype.render = function(e) { - var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button; - $currentTarget = $(e.currentTarget); - - if ($currentTarget.hasClass('js-no-comment-btn')) return; - - lineContentElement = this.getLineContent($currentTarget); - buttonParentElement = this.getButtonParent($currentTarget); - - if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return; - - $button = $(COMMENT_BUTTON_CLASS, buttonParentElement); - buttonParentElement.addClass('is-over') - .nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over'); - - if ($button.length) { - return; +/* Developer beware! Do not add logic to showButton or hideButton + * that will force a reflow. Doing so will create a signficant performance + * bottleneck for pages with large diffs. For a comprehensive list of what + * causes reflows, visit https://gist.github.com/paulirish/5d52fb081b3570c81e3a + */ + +const LINE_NUMBER_CLASS = 'diff-line-num'; +const UNFOLDABLE_LINE_CLASS = 'js-unfold'; +const NO_COMMENT_CLASS = 'no-comment-btn'; +const EMPTY_CELL_CLASS = 'empty-cell'; +const OLD_LINE_CLASS = 'old_line'; +const LINE_COLUMN_CLASSES = `.${LINE_NUMBER_CLASS}, .line_content`; +const DIFF_CONTAINER_SELECTOR = '.files'; +const DIFF_EXPANDED_CLASS = 'diff-expanded'; + +export default { + init($diffFile) { + /* Caching is used only when the following members are *true*. This is because there are likely to be + * differently configured versions of diffs in the same session. However if these values are true, they + * will be true in all cases */ + + if (!this.userCanCreateNote) { + // data-can-create-note is an empty string when true, otherwise undefined + this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('can-create-note') === ''; } - textFileElement = this.getTextFileElement($currentTarget); - buttonParentElement.append(this.buildButton({ - discussionID: lineContentElement.attr('data-discussion-id'), - lineType: lineContentElement.attr('data-line-type'), - - noteableType: textFileElement.attr('data-noteable-type'), - noteableID: textFileElement.attr('data-noteable-id'), - commitID: textFileElement.attr('data-commit-id'), - noteType: lineContentElement.attr('data-note-type'), - - // LegacyDiffNote - lineCode: lineContentElement.attr('data-line-code'), - - // DiffNote - position: lineContentElement.attr('data-position') - })); - }; - - FilesCommentButton.prototype.hideButton = function(e) { - var $currentTarget = $(e.currentTarget); - var buttonParentElement = this.getButtonParent($currentTarget); - - buttonParentElement.removeClass('is-over') - .nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over'); - }; - - FilesCommentButton.prototype.buildButton = function(buttonAttributes) { - return $commentButtonTemplate.clone().attr({ - 'data-discussion-id': buttonAttributes.discussionID, - 'data-line-type': buttonAttributes.lineType, - - 'data-noteable-type': buttonAttributes.noteableType, - 'data-noteable-id': buttonAttributes.noteableID, - 'data-commit-id': buttonAttributes.commitID, - 'data-note-type': buttonAttributes.noteType, - - // LegacyDiffNote - 'data-line-code': buttonAttributes.lineCode, - - // DiffNote - 'data-position': buttonAttributes.position - }); - }; - - FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { - return hoveredElement.closest(TEXT_FILE_SELECTOR); - }; - - FilesCommentButton.prototype.getLineContent = function(hoveredElement) { - if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { - return hoveredElement; - } - if (!this.isParallelView) { - return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); - } else { - return $(hoveredElement).next("." + LINE_CONTENT_CLASS); + if (typeof notes !== 'undefined' && !this.isParallelView) { + this.isParallelView = notes.isParallelView && notes.isParallelView(); } - }; - FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { - if (!this.isParallelView) { - if (hoveredElement.hasClass(OLD_LINE_CLASS)) { - return hoveredElement; - } - return hoveredElement.parent().find("." + OLD_LINE_CLASS); - } else { - if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) { - return hoveredElement; - } - return $(hoveredElement).prev("." + LINE_NUMBER_CLASS); + if (this.userCanCreateNote) { + $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e)) + .on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e)); } - }; + }, - FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { - return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS); - }; + showButton(isParallelView, e) { + const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView); - FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { - return lineContentElement.attr('data-note-type') && lineContentElement.attr('data-note-type') !== ''; - }; + if (!this.validateButtonParent(buttonParentElement)) return; - return FilesCommentButton; -})(); + buttonParentElement.classList.add('is-over'); + buttonParentElement.nextElementSibling.classList.add('is-over'); + }, -$.fn.filesCommentButton = function() { - $commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>'); + hideButton(isParallelView, e) { + const buttonParentElement = this.getButtonParent(e.currentTarget, isParallelView); - if (!(this && (this.parent().data('can-create-note') != null))) { - return; - } - return this.each(function() { - if (!$.data(this, 'filesCommentButton')) { - return $.data(this, 'filesCommentButton', new FilesCommentButton($(this))); + buttonParentElement.classList.remove('is-over'); + buttonParentElement.nextElementSibling.classList.remove('is-over'); + }, + + getButtonParent(hoveredElement, isParallelView) { + if (isParallelView) { + if (!hoveredElement.classList.contains(LINE_NUMBER_CLASS)) { + return hoveredElement.previousElementSibling; + } + } else if (!hoveredElement.classList.contains(OLD_LINE_CLASS)) { + return hoveredElement.parentNode.querySelector(`.${OLD_LINE_CLASS}`); } - }); + return hoveredElement; + }, + + validateButtonParent(buttonParentElement) { + return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) && + !buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) && + !buttonParentElement.classList.contains(NO_COMMENT_CLASS) && + !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS); + }, }; diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 5838b1bdbb7..a81389ab088 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -2,8 +2,9 @@ import Filter from '~/droplab/plugins/filter'; import './filtered_search_dropdown'; class DropdownHint extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, tokenKeys, filter) { - super(droplab, dropdown, input, filter); + constructor(options = {}) { + const { input, tokenKeys } = options; + super(options); this.config = { Filter: { template: 'hint', diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index 34a9e34070c..2615d626c4c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -5,8 +5,9 @@ import Filter from '~/droplab/plugins/filter'; import './filtered_search_dropdown'; class DropdownNonUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, tokenKeys, filter, endpoint, symbol) { - super(droplab, dropdown, input, filter); + constructor(options = {}) { + const { input, endpoint, symbol } = options; + super(options); this.symbol = symbol; this.config = { Ajax: { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 65c1b2050ac..7246ccbb281 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -2,10 +2,12 @@ import AjaxFilter from '~/droplab/plugins/ajax_filter'; import './filtered_search_dropdown'; +import { addClassIfElementExists } from '../lib/utils/dom_utils'; class DropdownUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, tokenKeys, filter) { - super(droplab, dropdown, input, filter); + constructor(options = {}) { + const { tokenKeys } = options; + super(options); this.config = { AjaxFilter: { endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, @@ -32,8 +34,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { } hideCurrentUser() { - const currenUserItem = this.dropdown.querySelector('.js-current-user'); - currenUserItem.classList.add('hidden'); + addClassIfElementExists(this.dropdown.querySelector('.js-current-user'), 'hidden'); } itemClicked(e) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js index 4209ca0d6e2..9e9a9ef74be 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js @@ -1,7 +1,7 @@ const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; class FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { + constructor({ droplab, dropdown, input, filter }) { this.droplab = droplab; this.hookId = input && input.id; this.input = input; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 6bc6bc43f51..61cef435209 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -42,13 +42,19 @@ class FilteredSearchDropdownManager { milestone: { reference: null, gl: 'DropdownNonUser', - extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'], + extraArguments: { + endpoint: `${this.baseEndpoint}/milestones.json`, + symbol: '%', + }, element: this.container.querySelector('#js-dropdown-milestone'), }, label: { reference: null, gl: 'DropdownNonUser', - extraArguments: [`${this.baseEndpoint}/labels.json`, '~'], + extraArguments: { + endpoint: `${this.baseEndpoint}/labels.json`, + symbol: '~', + }, element: this.container.querySelector('#js-dropdown-label'), }, hint: { @@ -97,13 +103,19 @@ class FilteredSearchDropdownManager { let forceShowList = false; if (!mappingKey.reference) { - const dl = this.droplab; - const defaultArguments = - [null, dl, element, this.filteredSearchInput, this.filteredSearchTokenKeys, key]; - const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); + const defaultArguments = { + droplab: this.droplab, + dropdown: element, + input: this.filteredSearchInput, + tokenKeys: this.filteredSearchTokenKeys, + filter: key, + }; + const extraArguments = mappingKey.extraArguments || {}; + const glArguments = Object.assign({}, defaultArguments, extraArguments); // Passing glArguments to `new gl[glClass](<arguments>)` - mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); + mappingKey.reference = + new (Function.prototype.bind.apply(gl[glClass], [null, glArguments]))(); } if (firstLoad) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 8f547bd8f1f..7872e9e68ad 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -3,6 +3,7 @@ import RecentSearchesRoot from './recent_searches_root'; import RecentSearchesStore from './stores/recent_searches_store'; import RecentSearchesService from './services/recent_searches_service'; import eventHub from './event_hub'; +import { addClassIfElementExists } from '../lib/utils/dom_utils'; class FilteredSearchManager { constructor(page) { @@ -40,6 +41,10 @@ class FilteredSearchManager { return []; }) .then((searches) => { + if (!searches) { + return; + } + // Put any searches that may have come in before // we fetched the saved searches ahead of the already saved ones const resultantSearches = this.recentSearchesStore.setRecentSearches( @@ -223,11 +228,7 @@ class FilteredSearchManager { } addInputContainerFocus() { - const inputContainer = this.filteredSearchInput.closest('.filtered-search-box'); - - if (inputContainer) { - inputContainer.classList.add('focus'); - } + addClassIfElementExists(this.filteredSearchInput.closest('.filtered-search-box'), 'focus'); } removeInputContainerFocus(e) { @@ -487,6 +488,7 @@ class FilteredSearchManager { } searchState(e) { + e.preventDefault(); const target = e.currentTarget; // remove focus outline after click target.blur(); diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 105762cb1ba..6cb9cfe1382 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,8 +1,5 @@ -import emojiMap from 'emojis/digests.json'; -import emojiAliases from 'emojis/aliases.json'; -import { glEmojiTag } from '~/behaviors/gl_emoji'; -import glRegexp from '~/lib/utils/regexp'; -import AjaxCache from '~/lib/utils/ajax_cache'; +import glRegexp from './lib/utils/regexp'; +import AjaxCache from './lib/utils/ajax_cache'; function sanitize(str) { return str.replace(/<(?:.|\n)*?>/gm, ''); @@ -33,6 +30,7 @@ class GfmAutoComplete { this.input.each((i, input) => { const $input = $(input); $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); + $input.on('change.atwho', () => input.dispatchEvent(new Event('input'))); // This triggers at.js again // Needed for quick actions with suffixes (ex: /label ~) $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); @@ -375,7 +373,12 @@ class GfmAutoComplete { if (this.cachedData[at]) { this.loadData($input, at, this.cachedData[at]); } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { - this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); + import(/* webpackChunkName: 'emoji' */ './emoji') + .then(({ validEmojiNames, glEmojiTag }) => { + this.loadData($input, at, validEmojiNames); + GfmAutoComplete.glEmojiTag = glEmojiTag; + }) + .catch(() => { this.isLoadingData[at] = false; }); } else { AjaxCache.retrieve(this.dataSources[GfmAutoComplete.atTypeMap[at]], true) .then((data) => { @@ -398,6 +401,13 @@ class GfmAutoComplete { this.cachedData = {}; } + destroy() { + this.input.each((i, input) => { + const $input = $(input); + $input.atwho('destroy'); + }); + } + static isLoading(data) { let dataToInspect = data; if (data && data.length > 0) { @@ -423,12 +433,14 @@ GfmAutoComplete.atTypeMap = { }; // Emoji +GfmAutoComplete.glEmojiTag = null; GfmAutoComplete.Emoji = { templateFunction(name) { - return `<li> - ${name} ${glEmojiTag(name)} - </li> - `; + // glEmojiTag helper is loaded on-demand in fetchData() + if (GfmAutoComplete.glEmojiTag) { + return `<li>${name} ${GfmAutoComplete.glEmojiTag(name)}</li>`; + } + return `<li>${name}</li>`; }, }; // Team Members diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index dc9f114af99..4e8141b2956 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -21,6 +21,9 @@ function GLForm(form, enableGFM = false) { GLForm.prototype.destroy = function() { // Clean form listeners this.clearEventListeners(); + if (this.autoComplete) { + this.autoComplete.destroy(); + } return this.form.data('gl-form', null); }; @@ -33,7 +36,8 @@ GLForm.prototype.setupForm = function() { this.form.addClass('gfm-form'); // remove notify commit author checkbox for non-commit notes gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); - new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'), { + this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); + this.autoComplete.setup(this.form.find('.js-gfm-input'), { emojis: true, members: this.enableGFM, issues: this.enableGFM, diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js new file mode 100644 index 00000000000..1c379e9bb67 --- /dev/null +++ b/app/assets/javascripts/gpg_badges.js @@ -0,0 +1,15 @@ +export default class GpgBadges { + static fetch() { + const form = $('.commits-search-form'); + + $.get({ + url: form.data('signatures-path'), + data: form.serialize(), + }).done((response) => { + const badges = $('.js-loading-gpg-badge'); + response.signatures.forEach((signature) => { + badges.filter(`[data-commit-sha="${signature.commit_sha}"]`).replaceWith(signature.html); + }); + }); + } +} diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js index 462d792b8d5..3e483b69fd2 100644 --- a/app/assets/javascripts/group_name.js +++ b/app/assets/javascripts/group_name.js @@ -1,16 +1,19 @@ - +import Cookies from 'js-cookie'; import _ from 'underscore'; export default class GroupName { constructor() { - this.titleContainer = document.querySelector('.title-container'); - this.title = document.querySelector('.title'); - this.titleWidth = this.title.offsetWidth; - this.groupTitle = document.querySelector('.group-title'); - this.groups = document.querySelectorAll('.group-path'); - this.toggle = null; - this.isHidden = false; - this.init(); + this.titleContainer = document.querySelector('.js-title-container'); + this.title = this.titleContainer.querySelector('.title'); + + if (this.title) { + this.titleWidth = this.title.offsetWidth; + this.groupTitle = this.titleContainer.querySelector('.group-title'); + this.groups = this.titleContainer.querySelectorAll('.group-path'); + this.toggle = null; + this.isHidden = false; + this.init(); + } } init() { @@ -33,11 +36,20 @@ export default class GroupName { createToggle() { this.toggle = document.createElement('button'); + this.toggle.setAttribute('type', 'button'); this.toggle.className = 'text-expander group-name-toggle'; this.toggle.setAttribute('aria-label', 'Toggle full path'); - this.toggle.innerHTML = '...'; + if (Cookies.get('new_nav') === 'true') { + this.toggle.innerHTML = '<i class="fa fa-ellipsis-h" aria-hidden="true"></i>'; + } else { + this.toggle.innerHTML = '...'; + } this.toggle.addEventListener('click', this.toggleGroups.bind(this)); - this.titleContainer.insertBefore(this.toggle, this.title); + if (Cookies.get('new_nav') === 'true') { + this.title.insertBefore(this.toggle, this.groupTitle); + } else { + this.titleContainer.insertBefore(this.toggle, this.title); + } this.toggleGroups(); } diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index ff601db2aa6..00e1bd94c9c 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -99,8 +99,10 @@ document.addEventListener('DOMContentLoaded', () => { page: currentPath, }, document.title, currentPath); - this.updateGroups(response.json()); - this.updatePagination(response.headers); + return response.json().then((data) => { + this.updateGroups(data); + this.updatePagination(response.headers); + }); }) .catch(this.handleErrorResponse); }, @@ -114,18 +116,19 @@ document.addEventListener('DOMContentLoaded', () => { }, leaveGroup(group, collection) { this.service.leaveGroup(group.leavePath) + .then(resp => resp.json()) .then((response) => { $.scrollTo(0); this.store.removeGroup(group, collection); // eslint-disable-next-line no-new - new Flash(response.json().notice, 'notice'); + new Flash(response.notice, 'notice'); }) - .catch((response) => { + .catch((error) => { let message = 'An error occurred. Please try again.'; - if (response.status === 403) { + if (error.status === 403) { message = 'Failed to leave the group. Please make sure you are not the only owner'; } diff --git a/app/assets/javascripts/groups/stores/groups_store.js b/app/assets/javascripts/groups/stores/groups_store.js index f6dc4290fd5..6eab6083e8f 100644 --- a/app/assets/javascripts/groups/stores/groups_store.js +++ b/app/assets/javascripts/groups/stores/groups_store.js @@ -47,8 +47,8 @@ export default class GroupsStore { // Map groups to an object groups.map((group) => { - mappedGroups[group.id] = group; - mappedGroups[group.id].subGroups = {}; + mappedGroups[`id${group.id}`] = group; + mappedGroups[`id${group.id}`].subGroups = {}; return group; }); @@ -56,26 +56,27 @@ export default class GroupsStore { const currentGroup = mappedGroups[key]; if (currentGroup.parentId) { // If the group is not at the root level, add it to its parent array of subGroups. - const findParentGroup = mappedGroups[currentGroup.parentId]; + const findParentGroup = mappedGroups[`id${currentGroup.parentId}`]; if (findParentGroup) { - mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup; - mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups + mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup; + mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups } else if (parentGroup && parentGroup.id === currentGroup.parentId) { - tree[currentGroup.id] = currentGroup; + tree[`id${currentGroup.id}`] = currentGroup; } else { - // Means the groups hast no direct parent. - // Save for later processing, we will add them to its corresponding base group + // No parent found. We save it for later processing orphans.push(currentGroup); + + // Add to tree to preserve original order + tree[`id${currentGroup.id}`] = currentGroup; } } else { - // If the group is at the root level, add it to first level elements array. - tree[currentGroup.id] = currentGroup; + // If the group is at the top level, add it to first level elements array. + tree[`id${currentGroup.id}`] = currentGroup; } return key; }); - // Hopefully this array will be empty for most cases if (orphans.length) { orphans.map((orphan) => { let found = false; @@ -83,11 +84,23 @@ export default class GroupsStore { Object.keys(tree).map((key) => { const group = tree[key]; - if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) { + + if ( + group && + currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 && + // Make sure the currently selected orphan is not the same as the group + // we are checking here otherwise it will end up in an infinite loop + currentOrphan.id !== group.id + ) { group.subGroups[currentOrphan.id] = currentOrphan; group.isOpen = true; currentOrphan.isOrphan = true; found = true; + + // Delete if group was put at the top level. If not the group will be displayed twice. + if (tree[`id${currentOrphan.id}`]) { + delete tree[`id${currentOrphan.id}`]; + } } return key; @@ -95,7 +108,8 @@ export default class GroupsStore { if (!found) { currentOrphan.isOrphan = true; - tree[currentOrphan.id] = currentOrphan; + + tree[`id${currentOrphan.id}`] = currentOrphan; } return orphan; @@ -140,7 +154,7 @@ export default class GroupsStore { // eslint-disable-next-line class-methods-use-this removeGroup(group, collection) { - Vue.delete(collection, group.id); + Vue.delete(collection, `id${group.id}`); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/helpers/issuables_helper.js b/app/assets/javascripts/helpers/issuables_helper.js new file mode 100644 index 00000000000..52d0f7e43fc --- /dev/null +++ b/app/assets/javascripts/helpers/issuables_helper.js @@ -0,0 +1,27 @@ +import CloseReopenReportToggle from '../close_reopen_report_toggle'; + +function initCloseReopenReport() { + const container = document.querySelector('.js-issuable-close-dropdown'); + + if (!container) return undefined; + + const dropdownTrigger = container.querySelector('.js-issuable-close-toggle'); + const dropdownList = container.querySelector('.js-issuable-close-menu'); + const button = container.querySelector('.js-issuable-close-button'); + + const closeReopenReportToggle = new CloseReopenReportToggle({ + dropdownTrigger, + dropdownList, + button, + }); + + closeReopenReportToggle.initDroplab(); + + return closeReopenReportToggle; +} + +const IssuablesHelper = { + initCloseReopenReport, +}; + +export default IssuablesHelper; diff --git a/app/assets/javascripts/how_to_merge.js b/app/assets/javascripts/how_to_merge.js new file mode 100644 index 00000000000..19f4a946f73 --- /dev/null +++ b/app/assets/javascripts/how_to_merge.js @@ -0,0 +1,12 @@ +document.addEventListener('DOMContentLoaded', () => { + const modal = $('#modal_merge_info').modal({ + modal: true, + show: false, + }); + $('.how_to_merge_link').on('click', () => { + modal.show(); + }); + $('.modal-header .close').on('click', () => { + modal.hide(); + }); +}); diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js new file mode 100644 index 00000000000..29e3d2ea94e --- /dev/null +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -0,0 +1,18 @@ +/* eslint-disable no-new */ +/* global MilestoneSelect */ +/* global LabelsSelect */ +/* global IssuableContext */ +/* global Sidebar */ + +export default () => { + const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); + + new MilestoneSelect({ + full_path: sidebarOptions.fullPath, + }); + new LabelsSelect(); + new IssuableContext(sidebarOptions.currentUser); + gl.Subscription.bindAll('.subscription'); + new gl.DueDateSelectors(); + window.sidebar = new Sidebar(); +}; diff --git a/app/assets/javascripts/init_legacy_filters.js b/app/assets/javascripts/init_legacy_filters.js new file mode 100644 index 00000000000..1211c2c802c --- /dev/null +++ b/app/assets/javascripts/init_legacy_filters.js @@ -0,0 +1,15 @@ +/* eslint-disable no-new */ +/* global LabelsSelect */ +/* global MilestoneSelect */ +/* global IssueStatusSelect */ +/* global SubscriptionSelect */ + +import UsersSelect from './users_select'; + +export default () => { + new UsersSelect(); + new LabelsSelect(); + new MilestoneSelect(); + new IssueStatusSelect(); + new SubscriptionSelect(); +}; diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js new file mode 100644 index 00000000000..3a8b4360cb6 --- /dev/null +++ b/app/assets/javascripts/init_notes.js @@ -0,0 +1,14 @@ +/* global Notes */ + +export default () => { + const dataEl = document.querySelector('.js-notes-data'); + const { + notesUrl, + notesIds, + now, + diffView, + autocomplete, + } = JSON.parse(dataEl.innerHTML); + + window.notes = new Notes(notesUrl, notesIds, now, diffView, autocomplete); +}; diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js index ddd3a6aab99..cf1e6a14725 100644 --- a/app/assets/javascripts/integrations/integration_settings_form.js +++ b/app/assets/javascripts/integrations/integration_settings_form.js @@ -102,7 +102,7 @@ export default class IntegrationSettingsForm { }) .done((res) => { if (res.error) { - new Flash(res.message, null, null, { + new Flash(`${res.message} ${res.service_response}`, null, null, { title: 'Save anyway', clickHandler: (e) => { e.preventDefault(); diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index a8856120c5e..d314f3c4d43 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -5,6 +5,7 @@ /* global SubscriptionSelect */ import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; +import SidebarHeightManager from './sidebar_height_manager'; const HIDDEN_CLASS = 'hidden'; const DISABLED_CONTENT_CLASS = 'disabled-content'; @@ -56,18 +57,6 @@ export default class IssuableBulkUpdateSidebar { return navbarHeight + layoutNavHeight + subNavScroll; } - initSidebar() { - if (!this.navHeight) { - this.navHeight = this.getNavHeight(); - } - - if (!this.sidebarInitialized) { - $(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this)); - $(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this)); - this.sidebarInitialized = true; - } - } - setupBulkUpdateActions() { IssuableBulkUpdateActions.setOriginalDropdownData(); } @@ -97,7 +86,20 @@ export default class IssuableBulkUpdateSidebar { this.toggleCheckboxDisplay(enable); if (enable) { - this.initSidebar(); + this.initAffix(); + SidebarHeightManager.init(); + } + } + + initAffix() { + if (!this.$sidebar.hasClass('affix-top')) { + const offsetTop = $('.scrolling-tabs-container').outerHeight() + $('.sub-nav-scroll').outerHeight(); + + this.$sidebar.affix({ + offset: { + top: offsetTop, + }, + }); } } @@ -143,17 +145,6 @@ export default class IssuableBulkUpdateSidebar { this.$bulkEditSubmitBtn.enable(); } } - // loosely based on method of the same name in right_sidebar.js - setSidebarHeight() { - const currentScrollDepth = window.pageYOffset || 0; - const diff = this.navHeight - currentScrollDepth; - - if (diff > 0) { - this.$sidebar.outerHeight(window.innerHeight - diff); - } else { - this.$sidebar.outerHeight('100%'); - } - } static getCheckedIssueIds() { const $checkedIssues = $('.selected_issue:checked'); diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index a4d7bf096ef..26392db4b5b 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -4,6 +4,8 @@ import Cookies from 'js-cookie'; import UsersSelect from './users_select'; +const PARTICIPANTS_ROW_COUNT = 7; + (function() { this.IssuableContext = (function() { function IssuableContext(currentUser) { @@ -50,11 +52,9 @@ import UsersSelect from './users_select'; } IssuableContext.prototype.initParticipants = function() { - var _this; - _this = this; $(document).on("click", ".js-participants-more", this.toggleHiddenParticipants); return $(".js-participants-author").each(function(i) { - if (i >= _this.PARTICIPANTS_ROW_COUNT) { + if (i >= PARTICIPANTS_ROW_COUNT) { return $(this).addClass("js-participants-hidden").hide(); } }); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 92f6f0d4117..9ac1325fc95 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,12 +1,12 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* global GitLab */ -/* global ZenMode */ /* global Autosave */ /* global dateFormat */ /* global Pikaday */ import UsersSelect from './users_select'; import GfmAutoComplete from './gfm_auto_complete'; +import ZenMode from './zen_mode'; (function() { this.IssuableForm = (function() { diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 0860e237ce1..2bee4fb045a 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -4,13 +4,14 @@ import 'vendor/jquery.waitforimages'; import '~/lib/utils/text_utility'; import './flash'; -import './task_list'; +import TaskList from './task_list'; import CreateMergeRequestDropdown from './create_merge_request_dropdown'; +import IssuablesHelper from './helpers/issuables_helper'; class Issue { constructor() { if ($('a.btn-close').length) { - this.taskList = new gl.TaskList({ + this.taskList = new TaskList({ dataType: 'issue', fieldName: 'description', selector: '.detail-page-description', @@ -28,6 +29,11 @@ class Issue { Issue.initMergeRequests(); Issue.initRelatedBranches(); + this.closeButtons = $('a.btn-close'); + this.reopenButtons = $('a.btn-reopen'); + + this.initCloseReopenReport(); + if (Issue.createMrDropdownWrap) { this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap); } @@ -35,13 +41,8 @@ class Issue { initIssueBtnEventListeners() { const issueFailMessage = 'Unable to update this issue at this time.'; - const closeButtons = $('a.btn-close'); - const isClosedBadge = $('div.status-box-closed'); - const isOpenBadge = $('div.status-box-open'); - const projectIssuesCounter = $('.issue_counter'); - const reopenButtons = $('a.btn-reopen'); - return closeButtons.add(reopenButtons).on('click', (e) => { + return $(document).on('click', 'a.btn-close, a.btn-reopen', (e) => { var $button, shouldSubmit, url; e.preventDefault(); e.stopImmediatePropagation(); @@ -50,7 +51,9 @@ class Issue { if (shouldSubmit) { Issue.submitNoteForm($button.closest('form')); } - $button.prop('disabled', true); + + this.disableCloseReopenButton($button); + url = $button.attr('href'); return $.ajax({ type: 'PUT', @@ -58,15 +61,19 @@ class Issue { }) .fail(() => new Flash(issueFailMessage)) .done((data) => { + const isClosedBadge = $('div.status-box-closed'); + const isOpenBadge = $('div.status-box-open'); + const projectIssuesCounter = $('.issue_counter'); + if ('id' in data) { $(document).trigger('issuable:change'); const isClosed = $button.hasClass('btn-close'); - closeButtons.toggleClass('hidden', isClosed); - reopenButtons.toggleClass('hidden', !isClosed); isClosedBadge.toggleClass('hidden', !isClosed); isOpenBadge.toggleClass('hidden', isClosed); + this.toggleCloseReopenButton(isClosed); + let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, '')); numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1; projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues)); @@ -83,12 +90,34 @@ class Issue { } else { new Flash(issueFailMessage); } - - $button.prop('disabled', false); + }) + .then(() => { + this.disableCloseReopenButton($button, false); }); }); } + initCloseReopenReport() { + this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport(); + + if (this.closeButtons) this.closeButtons = this.closeButtons.not('.issuable-close-button'); + if (this.reopenButtons) this.reopenButtons = this.reopenButtons.not('.issuable-close-button'); + } + + disableCloseReopenButton($button, shouldDisable) { + if (this.closeReopenReportToggle) { + this.closeReopenReportToggle.setDisable(shouldDisable); + } else { + $button.prop('disabled', shouldDisable); + } + } + + toggleCloseReopenButton(isClosed) { + if (this.closeReopenReportToggle) this.closeReopenReportToggle.updateButton(isClosed); + this.closeButtons.toggleClass('hidden', isClosed); + this.reopenButtons.toggleClass('hidden', !isClosed); + } + static submitNoteForm(form) { var noteText; noteText = form.find("textarea.js-note-text").val(); diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 8473a81bc88..efae112923d 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -202,16 +202,7 @@ export default { this.poll = new Poll({ resource: this.service, method: 'getData', - successCallback: (res) => { - const data = res.json(); - const shouldUpdate = this.store.stateShouldUpdate(data); - - this.store.updateState(data); - - if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) { - this.store.formState.lockedWarningVisible = true; - } - }, + successCallback: res => res.json().then(data => this.store.updateState(data)), errorCallback(err) { throw new Error(err); }, diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 43db66c8e08..48bad8f1e68 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,5 +1,6 @@ <script> import animateMixin from '../mixins/animate'; + import TaskList from '../../task_list'; export default { mixins: [animateMixin], @@ -46,7 +47,7 @@ if (this.canUpdate) { // eslint-disable-next-line no-new - new gl.TaskList({ + new TaskList({ dataType: 'issue', fieldName: 'description', selector: '.detail-page-description', diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 54650d2f184..27b1b814f9a 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -47,7 +47,8 @@ ref="textarea" slot="textarea" placeholder="Write a comment or drag your files here..." - @keydown.meta.enter="updateIssuable"> + @keydown.meta.enter="updateIssuable" + @keydown.ctrl.enter="updateIssuable"> </textarea> </markdown-field> </div> diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue index f811fb0de24..7bf2be8b28a 100644 --- a/app/assets/javascripts/issue_show/components/fields/project_move.vue +++ b/app/assets/javascripts/issue_show/components/fields/project_move.vue @@ -1,10 +1,10 @@ <script> - import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + import tooltip from '../../../vue_shared/directives/tooltip'; export default { - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, props: { formState: { type: Object, @@ -71,9 +71,9 @@ data-placeholder="Move to a different project" /> </div> <span + v-tooltip data-placement="auto top" - title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location." - ref="tooltip"> + title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."> <i class="fa fa-question-circle" aria-hidden="true"> diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue index 6556bf117e2..83af8e1e245 100644 --- a/app/assets/javascripts/issue_show/components/fields/title.vue +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -26,6 +26,7 @@ placeholder="Issue title" aria-label="Issue title" v-model="formState.title" - @keydown.meta.enter="updateIssuable" /> + @keydown.meta.enter="updateIssuable" + @keydown.ctrl.enter="updateIssuable" /> </fieldset> </template> diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index f2b822f3cbb..0c8bd6f1cc3 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -12,6 +12,10 @@ export default class Store { } updateState(data) { + if (this.stateShouldUpdate(data)) { + this.formState.lockedWarningVisible = true; + } + this.state.titleHtml = data.title; this.state.titleText = data.title_text; this.state.descriptionHtml = data.description; @@ -23,10 +27,8 @@ export default class Store { } stateShouldUpdate(data) { - return { - title: this.state.titleText !== data.title_text, - description: this.state.descriptionText !== data.description_text, - }; + return this.state.titleText !== data.title_text || + this.state.descriptionText !== data.description_text; } setFormState(state) { diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index 4223a8fea49..d0145fed396 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -39,6 +39,17 @@ runnerId() { return `#${this.job.runner.id}`; }, + renderBlock() { + return this.job.merge_request || + this.job.duration || + this.job.finished_data || + this.job.erased_at || + this.job.queued || + this.job.runner || + this.job.coverage || + this.job.tags.length || + this.job.cancel_path; + }, }, }; </script> @@ -63,7 +74,7 @@ Retry </a> </div> - <div class="block"> + <div :class="{block : renderBlock }"> <p class="build-detail-row js-job-mr" v-if="job.merge_request"> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index 939d17129de..f92e669414a 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -26,14 +26,6 @@ document.addEventListener('DOMContentLoaded', () => { mounted() { this.mediator.initBuildClass(); }, - updated() { - // Wait for flash message to be appended - Vue.nextTick(() => { - if (this.mediator.build) { - this.mediator.build.verifyTopPosition(); - } - }); - }, render(createElement) { return createElement('job-header', { props: { diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js index 063c52fac74..cc014b815c4 100644 --- a/app/assets/javascripts/jobs/job_details_mediator.js +++ b/app/assets/javascripts/jobs/job_details_mediator.js @@ -54,9 +54,8 @@ export default class JobMediator { } successCallback(response) { - const data = response.json(); this.state.isLoading = false; - this.store.storeJob(data); + return response.json().then(data => this.store.storeJob(data)); } errorCallback() { diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 38b2eb9ff14..d8814802d9e 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -21,6 +21,7 @@ } bindEvents() { + this.prioritizedLabels.find('.btn-action').on('mousedown', this, this.onButtonActionClick); return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick); } @@ -36,6 +37,11 @@ _this.toggleEmptyState($label, $btn, action); } + onButtonActionClick(e) { + e.stopPropagation(); + $(e.currentTarget).tooltip('hide'); + } + toggleEmptyState($label, $btn, action) { this.emptyState.classList.toggle('hidden', !!this.prioritizedLabels[0].querySelector(':scope > li')); } diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 71064ccc539..6186ffe20b3 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */ import _ from 'underscore'; +import Cookies from 'js-cookie'; +import NewNavSidebar from './new_sidebar'; (function() { var hideEndFade; @@ -53,6 +55,11 @@ import _ from 'underscore'; } $(() => { + if (Cookies.get('new_nav') === 'true') { + const newNavSidebar = new NewNavSidebar(); + newNavSidebar.bindEvents(); + } + $(window).on('scroll', _.throttle(applyScrollNavClass, 100)); }); }).call(window); diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js new file mode 100644 index 00000000000..3d64b121fa7 --- /dev/null +++ b/app/assets/javascripts/lazy_loader.js @@ -0,0 +1,76 @@ +/* eslint-disable one-export, one-var, one-var-declaration-per-line */ + +import _ from 'underscore'; + +export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; +const SCROLL_THRESHOLD = 300; + +export default class LazyLoader { + constructor(options = {}) { + this.lazyImages = []; + this.observerNode = options.observerNode || '#content-body'; + + const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300); + const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300); + + window.addEventListener('scroll', throttledScrollCheck); + window.addEventListener('resize', debouncedElementsInView); + + const scrollContainer = options.scrollContainer || window; + scrollContainer.addEventListener('load', () => this.loadCheck()); + } + searchLazyImages() { + this.lazyImages = [].slice.call(document.querySelectorAll('.lazy')); + this.checkElementsInView(); + } + startContentObserver() { + const contentNode = document.querySelector(this.observerNode) || document.querySelector('body'); + + if (contentNode) { + const observer = new MutationObserver(() => this.searchLazyImages()); + + observer.observe(contentNode, { + childList: true, + subtree: true, + }); + } + } + loadCheck() { + this.searchLazyImages(); + this.startContentObserver(); + } + scrollCheck() { + requestAnimationFrame(() => this.checkElementsInView()); + } + checkElementsInView() { + const scrollTop = pageYOffset; + const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD; + let imgBoundRect, imgTop, imgBound; + + // Loading Images which are in the current viewport or close to them + this.lazyImages = this.lazyImages.filter((selectedImage) => { + if (selectedImage.getAttribute('data-src')) { + imgBoundRect = selectedImage.getBoundingClientRect(); + + imgTop = scrollTop + imgBoundRect.top; + imgBound = imgTop + imgBoundRect.height; + + if (scrollTop < imgBound && visHeight > imgTop) { + LazyLoader.loadImage(selectedImage); + return false; + } + + return true; + } + return false; + }); + } + static loadImage(img) { + if (img.getAttribute('data-src')) { + img.setAttribute('src', img.getAttribute('data-src')); + img.removeAttribute('data-src'); + img.classList.remove('lazy'); + img.classList.add('js-lazy-loaded'); + } + } +} diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 2aca86189fd..122ec138c59 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -86,18 +86,25 @@ // This is required to handle non-unicode characters in hash hash = decodeURIComponent(hash); + var fixedTabs = document.querySelector('.js-tabs-affix'); + var fixedNav = document.querySelector('.navbar-gitlab'); + + var adjustment = 0; + if (fixedNav) adjustment -= fixedNav.offsetHeight; + // scroll to user-generated markdown anchor if we cannot find a match if (document.getElementById(hash) === null) { var target = document.getElementById('user-content-' + hash); if (target && target.scrollIntoView) { target.scrollIntoView(true); + window.scrollBy(0, adjustment); } } else { // only adjust for fixedTabs when not targeting user-generated content - var fixedTabs = document.querySelector('.js-tabs-affix'); if (fixedTabs) { - window.scrollBy(0, -fixedTabs.offsetHeight); + adjustment -= fixedTabs.offsetHeight; } + window.scrollBy(0, adjustment); } }; diff --git a/app/assets/javascripts/lib/utils/datefix.js b/app/assets/javascripts/lib/utils/datefix.js new file mode 100644 index 00000000000..990dc3f6d1a --- /dev/null +++ b/app/assets/javascripts/lib/utils/datefix.js @@ -0,0 +1,8 @@ +const DateFix = { + dashedFix(val) { + const [y, m, d] = val.split('-'); + return new Date(y, m - 1, d); + }, +}; + +export default DateFix; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index bfcc50996cc..1d1763c3963 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -112,29 +112,11 @@ window.dateFormat = dateFormat; return timefor; }; - w.gl.utils.cachedTimeagoElements = []; w.gl.utils.renderTimeago = function($els) { - if (!$els && !w.gl.utils.cachedTimeagoElements.length) { - w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render')); - } else if ($els) { - w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray()); - } - - w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText); - }; - - w.gl.utils.updateTimeagoText = function(el) { - const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), lang); - - if (el.textContent !== formattedDate) { - el.textContent = formattedDate; - } - }; - - w.gl.utils.initTimeagoTimeout = function() { - gl.utils.renderTimeago(); + const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); - gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000); + // timeago.js sets timeouts internally for each timeago value to be updated in real time + gl.utils.getTimeago().render(timeagoEls, lang); }; w.gl.utils.getDayDifference = function(a, b) { diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js new file mode 100644 index 00000000000..de65ea15a60 --- /dev/null +++ b/app/assets/javascripts/lib/utils/dom_utils.js @@ -0,0 +1,7 @@ +/* eslint-disable import/prefer-default-export */ + +export const addClassIfElementExists = (element, className) => { + if (element) { + element.classList.add(className); + } +}; diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index 415e50f32ae..625e53ee9de 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -3,6 +3,7 @@ */ export default { + ABORTED: 0, NO_CONTENT: 204, OK: 200, }; diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js index e31cc5fbabe..97666e13ebe 100644 --- a/app/assets/javascripts/lib/utils/poll.js +++ b/app/assets/javascripts/lib/utils/poll.js @@ -81,6 +81,9 @@ export default class Poll { }) .catch((error) => { notificationCallback(false); + if (error.status === httpStatusCodes.ABORTED) { + return; + } errorCallback(error); }); } diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 601d01e1be1..021f936a4fa 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -94,8 +94,8 @@ gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; - if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { - if (blockTag != null) { + if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { + if (blockTag != null && blockTag !== '') { insertText = this.blockTagText(text, textArea, blockTag, selected); } else { insertText = selectedSplit.map(function(val) { diff --git a/app/assets/javascripts/locale/bg/app.js b/app/assets/javascripts/locale/bg/app.js deleted file mode 100644 index ba56c0bea25..00000000000 --- a/app/assets/javascripts/locale/bg/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['bg'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-05 09:40-0400","Last-Translator":"Lyubomir Vasilev <lyubomirv@abv.bg>","Language-Team":"Bulgarian","Language":"bg","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"bg","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["от"],"Commit":["Подаване","Подавания"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Анализът на циклите дава общ поглед върху това колко време е нужно на една идея да се превърне в завършена функционалност в проекта."],"CycleAnalyticsStage|Code":["Програмиране"],"CycleAnalyticsStage|Issue":["Проблем"],"CycleAnalyticsStage|Plan":["Планиране"],"CycleAnalyticsStage|Production":["Издаване"],"CycleAnalyticsStage|Review":["Преглед и одобрение"],"CycleAnalyticsStage|Staging":["Подготовка за издаване"],"CycleAnalyticsStage|Test":["Тестване"],"Deploy":["Внедряване","Внедрявания"],"FirstPushedBy|First":["Първо"],"FirstPushedBy|pushed by":["изпращане на промени от"],"From issue creation until deploy to production":["От създаването на проблема до внедряването в крайната версия"],"From merge request merge until deploy to production":["От прилагането на заявката за сливане до внедряването в крайната версия"],"Introducing Cycle Analytics":["Представяме Ви анализът на циклите"],"Last %d day":["Последния %d ден","Последните %d дни"],"Limited to showing %d event at most":["Ограничено до показване на последното %d събитие","Ограничено до показване на последните %d събития"],"Median":["Медиана"],"New Issue":["Нов проблем","Нови проблема"],"Not available":["Не е налично"],"Not enough data":["Няма достатъчно данни"],"OpenedNDaysAgo|Opened":["Отворен"],"Pipeline Health":["Състояние"],"ProjectLifecycle|Stage":["Етап"],"Read more":["Прочетете повече"],"Related Commits":["Свързани подавания"],"Related Deployed Jobs":["Свързани задачи за внедряване"],"Related Issues":["Свързани проблеми"],"Related Jobs":["Свързани задачи"],"Related Merge Requests":["Свързани заявки за сливане"],"Related Merged Requests":["Свързани приложени заявки за сливане"],"Showing %d event":["Показване на %d събитие","Показване на %d събития"],"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.":["Етапът на програмиране показва времето от първото подаване до създаването на заявката за сливане. Данните ще бъдат добавени тук автоматично след като бъде създадена първата заявка за сливане."],"The collection of events added to the data gathered for that stage.":["Съвкупността от събития добавени към данните събрани за този етап."],"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.":["Етапът на проблемите показва колко е времето от създаването на проблем до определянето на целеви етап на проекта за него, или до добавянето му в списък на дъската за проблеми. Започнете да добавяте проблеми, за да видите данните за този етап."],"The phase of the development lifecycle.":["Етапът от цикъла на разработка"],"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.":["Етапът на планиране показва колко е времето от преходната стъпка до изпращането на първото подаване. Това време ще бъде добавено автоматично след като изпратите първото си подаване."],"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.":["Етапът на издаване показва общото време, което е нужно от създаването на проблем до внедряването на кода в крайната версия."],"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.":["Етапът на преглед и одобрение показва времето от създаването на заявката за сливане до прилагането ѝ. Данните ще бъдат добавени автоматично след като приложите първата си заявка за сливане."],"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.":["Етапът на подготовка за издаване показва времето между прилагането на заявката за сливане и внедряването на кода в средата на работещата крайна версия. Данните ще бъдат добавени автоматично след като направите първото си внедряване в крайната версия."],"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.":["Етапът на тестване показва времето, което е нужно на „Gitlab CI“ да изпълни всички задачи за свързаната заявка за сливане. Данните ще бъдат добавени автоматично след като приключи изпълнените на първата Ви такава задача."],"The time taken by each data entry gathered by that stage.":["Времето, което отнема всеки запис от данни за съответния етап."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Стойността, която се намира в средата на последователността от наблюдавани данни. Например: медианата на 3, 5 и 9 е 5, а медианата на 3, 5, 7 и 8 е (5+7)/2 = 6."],"Time before an issue gets scheduled":["Време преди един проблем да бъде планиран за работа"],"Time before an issue starts implementation":["Време преди работата по проблем да започне"],"Time between merge request creation and merge/close":["Време между създаване на заявка за сливане и прилагането/отхвърлянето ѝ"],"Time until first merge request":["Време преди първата заявка за сливане"],"Time|hr":["час","часа"],"Time|min":["мин","мин"],"Time|s":["сек"],"Total Time":["Общо време"],"Total test time for all commits/merges":["Общо време за тестване на всички подавания/сливания"],"Want to see the data? Please ask an administrator for access.":["Искате ли да видите данните? Помолете администратор за достъп."],"We don't have enough data to show this stage.":["Няма достатъчно данни за този етап."],"You need permission.":["Нуждаете се от разрешение."],"day":["ден","дни"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/de/app.js b/app/assets/javascripts/locale/de/app.js deleted file mode 100644 index e7d2b174405..00000000000 --- a/app/assets/javascripts/locale/de/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['de'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-09 13:44+0200","Language-Team":"German","Language":"de","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"de","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["Von"],"Cancel":[""],"Commit":["Commit","Commits"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics liefern einen Überblick darüber, wie viel Zeit in Ihrem Projekt von einer Idee bis zum Produktivdeployment vergeht."],"CycleAnalyticsStage|Code":["Code"],"CycleAnalyticsStage|Issue":["Issue"],"CycleAnalyticsStage|Plan":["Planung"],"CycleAnalyticsStage|Production":["Produktiv"],"CycleAnalyticsStage|Review":["Review"],"CycleAnalyticsStage|Staging":["Staging"],"CycleAnalyticsStage|Test":["Test"],"Delete":[""],"Deploy":["Deployment","Deployments"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["Erster"],"FirstPushedBy|pushed by":["gepusht von"],"From issue creation until deploy to production":["Vom Anlegen des Issues bis zum Produktivdeployment"],"From merge request merge until deploy to production":["Vom Merge Request bis zum Produktivdeployment"],"Interval Pattern":[""],"Introducing Cycle Analytics":["Was sind Cycle Analytics?"],"Last %d day":["Letzter %d Tag","Letzten %d Tage"],"Last Pipeline":[""],"Limited to showing %d event at most":["Eingeschränkt auf maximal %d Ereignis","Eingeschränkt auf maximal %d Ereignisse"],"Median":["Median"],"New Issue":["Neues Issue","Neue Issues"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["Nicht verfügbar"],"Not enough data":["Nicht genügend Daten"],"OpenedNDaysAgo|Opened":["Erstellt"],"Owner":[""],"Pipeline Health":["Pipeline Kennzahlen"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["Phase"],"Read more":["Mehr"],"Related Commits":["Zugehörige Commits"],"Related Deployed Jobs":["Zugehörige Deploymentjobs"],"Related Issues":["Zugehörige Issues"],"Related Jobs":["Zugehörige Jobs"],"Related Merge Requests":["Zugehörige Merge Requests"],"Related Merged Requests":["Zugehörige abgeschlossene Merge Requests"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["Zeige %d Ereignis","Zeige %d Ereignisse"],"Target Branch":[""],"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.":["Die Code-Phase stellt die Zeit vom ersten Commit bis zum Erstellen eines Merge Requests dar. Sobald Sie Ihren ersten Merge Request anlegen, werden dessen Daten automatisch ergänzt."],"The collection of events added to the data gathered for that stage.":["Ereignisse, die für diese Phase ausgewertet wurden."],"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.":["Die Issue-Phase stellt die Zeit vom Anlegen eines Issues bis zum Zuweisen eines Meilensteins oder Hinzufügen zum Issue Board dar. Erstellen Sie einen Issue, damit dessen Daten hier erscheinen."],"The phase of the development lifecycle.":["Die Phase im Entwicklungsprozess."],"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.":["Die Planungsphase stellt die Zeit von der vorherigen Phase bis zum Pushen des ersten Commits dar. Sobald Sie den ersten Commit pushen, werden dessen Daten hier erscheinen."],"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.":["Die Produktiv-Phase stellt die Gesamtzeit vom Anlegen eines Issues bis zum Deployment auf dem Produktivsystem dar. Sobald Sie den vollständigen Entwicklungszyklus von einer Idee bis zum Produktivdeployment durchlaufen haben, erscheinen die zugehörigen Daten hier."],"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.":["Die Review-Phase stellt die Zeit vom Anlegen eines Merge Requests bis zum Mergen dar. Sobald Sie Ihren ersten Merge Request abschließen, werden dessen Daten hier automatisch angezeigt."],"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.":["Die Staging-Phase stellt die Zeit zwischen Mergen eines Merge Requests und dem Produktivdeployment dar. Sobald Sie das erste Produktivdeployment durchgeführt haben, werden dessen Daten hier automatisch angezeigt."],"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.":["Die Test-Phase stellt die Zeit dar, die GitLab CI benötigt um die Pipelines von Merge Requests abzuarbeiten. Sobald die erste Pipeline abgeschlossen ist, werden deren Daten hier automatisch angezeigt."],"The time taken by each data entry gathered by that stage.":["Zeit die für das jeweilige Ereignis in der Phase ermittelt wurde."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["Der mittlere aller erfassten Werte. Zum Beispiel ist für 3, 5, 9 der Median 5. Bei 3, 5, 7, 8 ist der Median (5+7)/2 = 6."],"Time before an issue gets scheduled":["Zeit bis ein Issue geplant wird"],"Time before an issue starts implementation":["Zeit bis die Implementierung für ein Issue beginnt"],"Time between merge request creation and merge/close":["Zeit zwischen Anlegen und Mergen/Schließen eines Merge Requests"],"Time until first merge request":["Zeit bis zum ersten Merge Request"],"Time|hr":["h","h"],"Time|min":["min","min"],"Time|s":["s"],"Total Time":["Gesamtzeit"],"Total test time for all commits/merges":["Gesamte Testlaufzeit für alle Commits/Merges"],"Want to see the data? Please ask an administrator for access.":["Um diese Daten einsehen zu können, wenden Sie sich bitte an Ihren Administrator."],"We don't have enough data to show this stage.":["Es liegen nicht genügend Daten vor, um diese Phase anzuzeigen."],"You need permission.":["Sie benötigen Zugriffsrechte."],"day":["Tag","Tage"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/en/app.js b/app/assets/javascripts/locale/en/app.js deleted file mode 100644 index d634af959e5..00000000000 --- a/app/assets/javascripts/locale/en/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['en'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-04-12 22:36-0500","Last-Translator":"FULL NAME <EMAIL@ADDRESS>","Language-Team":"English","Language":"en","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","lang":"en","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"%{commit_author_link} committed %{commit_timeago}":[""],"About auto deploy":[""],"Active":[""],"Activity":[""],"Add Changelog":[""],"Add Contribution guide":[""],"Add License":[""],"Add an SSH key to your profile to pull or push via SSH.":[""],"Add new directory":[""],"Archived project! Repository is read-only":[""],"Are you sure you want to delete this pipeline schedule?":[""],"Attach a file by drag & drop or %{upload_link}":[""],"Branch":["",""],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":[""],"Branches":[""],"Browse files":[""],"ByAuthor|by":[""],"CI configuration":[""],"Cancel":[""],"ChangeTypeActionLabel|Pick into branch":[""],"ChangeTypeActionLabel|Revert in branch":[""],"ChangeTypeAction|Cherry-pick":[""],"Changelog":[""],"Charts":[""],"Cherry-pick this commit":[""],"Cherry-pick this merge request":[""],"CiStatusLabel|canceled":[""],"CiStatusLabel|created":[""],"CiStatusLabel|failed":[""],"CiStatusLabel|manual action":[""],"CiStatusLabel|passed":[""],"CiStatusLabel|passed with warnings":[""],"CiStatusLabel|pending":[""],"CiStatusLabel|skipped":[""],"CiStatusLabel|waiting for manual action":[""],"CiStatusText|blocked":[""],"CiStatusText|canceled":[""],"CiStatusText|created":[""],"CiStatusText|failed":[""],"CiStatusText|manual":[""],"CiStatusText|passed":[""],"CiStatusText|pending":[""],"CiStatusText|skipped":[""],"CiStatus|running":[""],"Commit":["",""],"Commit message":[""],"CommitBoxTitle|Commit":[""],"CommitMessage|Add %{file_name}":[""],"Commits":[""],"Commits|History":[""],"Committed by":[""],"Compare":[""],"Contribution guide":[""],"Contributors":[""],"Copy URL to clipboard":[""],"Copy commit SHA to clipboard":[""],"Create New Directory":[""],"Create directory":[""],"Create empty bare repository":[""],"Create merge request":[""],"Create new...":[""],"CreateNewFork|Fork":[""],"CreateTag|Tag":[""],"Cron Timezone":[""],"Cron syntax":[""],"Custom notification events":[""],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":[""],"Cycle Analytics":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":[""],"CycleAnalyticsStage|Code":[""],"CycleAnalyticsStage|Issue":[""],"CycleAnalyticsStage|Plan":[""],"CycleAnalyticsStage|Production":[""],"CycleAnalyticsStage|Review":[""],"CycleAnalyticsStage|Staging":[""],"CycleAnalyticsStage|Test":[""],"Define a custom pattern with cron syntax":[""],"Delete":[""],"Deploy":["",""],"Description":[""],"Directory name":[""],"Don't show again":[""],"Download":[""],"Download tar":[""],"Download tar.bz2":[""],"Download tar.gz":[""],"Download zip":[""],"DownloadArtifacts|Download":[""],"DownloadCommit|Email Patches":[""],"DownloadCommit|Plain Diff":[""],"DownloadSource|Download":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Every day (at 4:00am)":[""],"Every month (on the 1st at 4:00am)":[""],"Every week (Sundays at 4:00am)":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Files":[""],"Find by path":[""],"Find file":[""],"FirstPushedBy|First":[""],"FirstPushedBy|pushed by":[""],"Fork":["",""],"ForkedFromProjectPath|Forked from":[""],"From issue creation until deploy to production":[""],"From merge request merge until deploy to production":[""],"Go to your fork":[""],"GoToYourFork|Fork":[""],"Home":[""],"Housekeeping successfully started":[""],"Import repository":[""],"Interval Pattern":[""],"Introducing Cycle Analytics":[""],"LFSStatus|Disabled":[""],"LFSStatus|Enabled":[""],"Last %d day":["",""],"Last Pipeline":[""],"Last Update":[""],"Last commit":[""],"Learn more in the":[""],"Learn more in the|pipeline schedules documentation":[""],"Leave group":[""],"Leave project":[""],"Limited to showing %d event at most":["",""],"Median":[""],"MissingSSHKeyWarningLink|add an SSH key":[""],"New Issue":["",""],"New Pipeline Schedule":[""],"New branch":[""],"New directory":[""],"New file":[""],"New issue":[""],"New merge request":[""],"New schedule":[""],"New snippet":[""],"New tag":[""],"No repository":[""],"No schedules":[""],"Not available":[""],"Not enough data":[""],"Notification events":[""],"NotificationEvent|Close issue":[""],"NotificationEvent|Close merge request":[""],"NotificationEvent|Failed pipeline":[""],"NotificationEvent|Merge merge request":[""],"NotificationEvent|New issue":[""],"NotificationEvent|New merge request":[""],"NotificationEvent|New note":[""],"NotificationEvent|Reassign issue":[""],"NotificationEvent|Reassign merge request":[""],"NotificationEvent|Reopen issue":[""],"NotificationEvent|Successful pipeline":[""],"NotificationLevel|Custom":[""],"NotificationLevel|Disabled":[""],"NotificationLevel|Global":[""],"NotificationLevel|On mention":[""],"NotificationLevel|Participate":[""],"NotificationLevel|Watch":[""],"OfSearchInADropdown|Filter":[""],"OpenedNDaysAgo|Opened":[""],"Options":[""],"Owner":[""],"Pipeline":[""],"Pipeline Health":[""],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"PipelineSheduleIntervalPattern|Custom":[""],"Pipeline|with stage":[""],"Pipeline|with stages":[""],"Project '%{project_name}' queued for deletion.":[""],"Project '%{project_name}' was successfully created.":[""],"Project '%{project_name}' was successfully updated.":[""],"Project '%{project_name}' will be deleted.":[""],"Project access must be granted explicitly to each user.":[""],"Project export could not be deleted.":[""],"Project export has been deleted.":[""],"Project export link has expired. Please generate a new export from your project settings.":[""],"Project export started. A download link will be sent by email.":[""],"Project home":[""],"ProjectFeature|Disabled":[""],"ProjectFeature|Everyone with access":[""],"ProjectFeature|Only team members":[""],"ProjectFileTree|Name":[""],"ProjectLastActivity|Never":[""],"ProjectLifecycle|Stage":[""],"ProjectNetworkGraph|Graph":[""],"Read more":[""],"Readme":[""],"RefSwitcher|Branches":[""],"RefSwitcher|Tags":[""],"Related Commits":[""],"Related Deployed Jobs":[""],"Related Issues":[""],"Related Jobs":[""],"Related Merge Requests":[""],"Related Merged Requests":[""],"Remind later":[""],"Remove project":[""],"Request Access":[""],"Revert this commit":[""],"Revert this merge request":[""],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Scheduling Pipelines":[""],"Search branches and tags":[""],"Select Archive Format":[""],"Select a timezone":[""],"Select target branch":[""],"Set a password on your account to pull or push via %{protocol}":[""],"Set up CI":[""],"Set up Koding":[""],"Set up auto deploy":[""],"SetPasswordToCloneLink|set a password":[""],"Showing %d event":["",""],"Source code":[""],"StarProject|Star":[""],"Start a %{new_merge_request} with these changes":[""],"Start a <strong>new merge request</strong> with these changes":[""],"Switch branch/tag":[""],"Tag":["",""],"Tags":[""],"Target Branch":[""],"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.":[""],"The collection of events added to the data gathered for that stage.":[""],"The fork relationship has been removed.":[""],"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.":[""],"The phase of the development lifecycle.":[""],"The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.":[""],"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.":[""],"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.":[""],"The project can be accessed by any logged in user.":[""],"The project can be accessed without any authentication.":[""],"The repository for this project does not exist.":[""],"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.":[""],"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.":[""],"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.":[""],"The time taken by each data entry gathered by that stage.":[""],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":[""],"This means you can not push code until you create an empty repository or import existing one.":[""],"Time before an issue gets scheduled":[""],"Time before an issue starts implementation":[""],"Time between merge request creation and merge/close":[""],"Time until first merge request":[""],"Timeago|%s days ago":[""],"Timeago|%s days remaining":[""],"Timeago|%s hours remaining":[""],"Timeago|%s minutes ago":[""],"Timeago|%s minutes remaining":[""],"Timeago|%s months ago":[""],"Timeago|%s months remaining":[""],"Timeago|%s seconds remaining":[""],"Timeago|%s weeks ago":[""],"Timeago|%s weeks remaining":[""],"Timeago|%s years ago":[""],"Timeago|%s years remaining":[""],"Timeago|1 day remaining":[""],"Timeago|1 hour remaining":[""],"Timeago|1 minute remaining":[""],"Timeago|1 month remaining":[""],"Timeago|1 week remaining":[""],"Timeago|1 year remaining":[""],"Timeago|Past due":[""],"Timeago|a day ago":[""],"Timeago|a month ago":[""],"Timeago|a week ago":[""],"Timeago|a while":[""],"Timeago|a year ago":[""],"Timeago|about %s hours ago":[""],"Timeago|about a minute ago":[""],"Timeago|about an hour ago":[""],"Timeago|in %s days":[""],"Timeago|in %s hours":[""],"Timeago|in %s minutes":[""],"Timeago|in %s months":[""],"Timeago|in %s seconds":[""],"Timeago|in %s weeks":[""],"Timeago|in %s years":[""],"Timeago|in 1 day":[""],"Timeago|in 1 hour":[""],"Timeago|in 1 minute":[""],"Timeago|in 1 month":[""],"Timeago|in 1 week":[""],"Timeago|in 1 year":[""],"Timeago|less than a minute ago":[""],"Time|hr":["",""],"Time|min":["",""],"Time|s":[""],"Total Time":[""],"Total test time for all commits/merges":[""],"Unstar":[""],"Upload New File":[""],"Upload file":[""],"Use your global notification setting":[""],"VisibilityLevel|Internal":[""],"VisibilityLevel|Private":[""],"VisibilityLevel|Public":[""],"Want to see the data? Please ask an administrator for access.":[""],"We don't have enough data to show this stage.":[""],"Withdraw Access Request":[""],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":[""],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":[""],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":[""],"You can only add files when you are on a branch":[""],"You must sign in to star a project":[""],"You need permission.":[""],"You will not get any notifications via email":[""],"You will only receive notifications for the events you choose":[""],"You will only receive notifications for threads you have participated in":[""],"You will receive notifications for any activity":[""],"You will receive notifications only for comments in which you were @mentioned":[""],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":[""],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":[""],"Your name":[""],"day":["",""],"new merge request":[""],"notification emails":[""],"parent":["",""]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/es/app.js b/app/assets/javascripts/locale/es/app.js deleted file mode 100644 index eafcd15acf9..00000000000 --- a/app/assets/javascripts/locale/es/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-06-15 21:59-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"Bob Van Landuyt <bob@gitlab.com>","X-Generator":"Poedit 2.0.2","POT-Creation-Date":"2017-06-15 21:59-0500","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"%{commit_author_link} committed %{commit_timeago}":["%{commit_author_link} cambió %{commit_timeago}"],"About auto deploy":["Acerca del auto despliegue"],"Active":["Activo"],"Activity":["Actividad"],"Add Changelog":["Agregar Changelog"],"Add Contribution guide":["Agregar guía de contribución"],"Add License":["Agregar Licencia"],"Add an SSH key to your profile to pull or push via SSH.":["Agregar una clave SSH a tu perfil para actualizar o enviar a través de SSH."],"Add new directory":["Agregar nuevo directorio"],"Archived project! Repository is read-only":["¡Proyecto archivado! El repositorio es de solo lectura"],"Are you sure you want to delete this pipeline schedule?":["¿Estás seguro que deseas eliminar esta programación del pipeline?"],"Attach a file by drag & drop or %{upload_link}":["Adjunte un archivo arrastrando & soltando o %{upload_link}"],"Branch":["Rama","Ramas"],"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":["La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"],"Branches":["Ramas"],"Browse files":["Examinar los archivos"],"ByAuthor|by":["por"],"CI configuration":["Configuración de CI"],"Cancel":["Cancelar"],"ChangeTypeActionLabel|Pick into branch":["Escoger en la rama"],"ChangeTypeActionLabel|Revert in branch":["Revertir en la rama"],"ChangeTypeAction|Cherry-pick":["Cherry-pick"],"ChangeTypeAction|Revert":["Revertir"],"Changelog":["Changelog"],"Charts":["Gráficos"],"Cherry-pick this commit":["Escoger este cambio"],"Cherry-pick this merge request":["Escoger esta solicitud de fusión"],"CiStatusLabel|canceled":["cancelado"],"CiStatusLabel|created":["creado"],"CiStatusLabel|failed":["fallido"],"CiStatusLabel|manual action":["acción manual"],"CiStatusLabel|passed":["pasó"],"CiStatusLabel|passed with warnings":["pasó con advertencias"],"CiStatusLabel|pending":["pendiente"],"CiStatusLabel|skipped":["omitido"],"CiStatusLabel|waiting for manual action":["esperando acción manual"],"CiStatusText|blocked":["bloqueado"],"CiStatusText|canceled":["cancelado"],"CiStatusText|created":["creado"],"CiStatusText|failed":["fallado"],"CiStatusText|manual":["manual"],"CiStatusText|passed":["pasó"],"CiStatusText|pending":["pendiente"],"CiStatusText|skipped":["omitido"],"CiStatus|running":["en ejecución"],"Commit":["Cambio","Cambios"],"Commit message":["Mensaje del cambio"],"CommitBoxTitle|Commit":["Cambio"],"CommitMessage|Add %{file_name}":["Agregar %{file_name}"],"Commits":["Cambios"],"Commits|History":["Historial"],"Committed by":["Enviado por"],"Compare":["Comparar"],"Contribution guide":["Guía de contribución"],"Contributors":["Contribuidores"],"Copy URL to clipboard":["Copiar URL al portapapeles"],"Copy commit SHA to clipboard":["Copiar SHA del cambio al portapapeles"],"Create New Directory":["Crear Nuevo Directorio"],"Create directory":["Crear directorio"],"Create empty bare repository":["Crear repositorio vacío"],"Create merge request":["Crear solicitud de fusión"],"Create new...":["Crear nuevo..."],"CreateNewFork|Fork":["Bifurcar"],"CreateTag|Tag":["Etiqueta"],"Cron Timezone":["Zona horaria del Cron"],"Cron syntax":["Sintaxis de Cron"],"Custom notification events":["Eventos de notificaciones personalizadas"],"Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}.":["Los niveles de notificación personalizados son los mismos que los niveles participantes. Con los niveles de notificación personalizados, también recibirá notificaciones para eventos seleccionados. Para obtener más información, consulte %{notification_link}."],"Cycle Analytics":["Cycle Analytics"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Define a custom pattern with cron syntax":["Definir un patrón personalizado con la sintaxis de cron"],"Delete":["Eliminar"],"Deploy":["Despliegue","Despliegues"],"Description":["Descripción"],"Directory name":["Nombre del directorio"],"Don't show again":["No mostrar de nuevo"],"Download":["Descargar"],"Download tar":["Descargar tar"],"Download tar.bz2":["Descargar tar.bz2"],"Download tar.gz":["Descargar tar.gz"],"Download zip":["Descargar zip"],"DownloadArtifacts|Download":["Descargar"],"DownloadCommit|Email Patches":["Parches por correo electrónico"],"DownloadCommit|Plain Diff":["Diferencias en texto plano"],"DownloadSource|Download":["Descargar"],"Edit":["Editar"],"Edit Pipeline Schedule %{id}":["Editar Programación del Pipeline %{id}"],"Every day (at 4:00am)":["Todos los días (a las 4:00 am)"],"Every month (on the 1st at 4:00am)":["Todos los meses (el día 1 a las 4:00 am)"],"Every week (Sundays at 4:00am)":["Todas las semanas (domingos a las 4:00 am)"],"Failed to change the owner":["Error al cambiar el propietario"],"Failed to remove the pipeline schedule":["Error al eliminar la programación del pipeline"],"Files":["Archivos"],"Find by path":["Buscar por ruta"],"Find file":["Buscar archivo"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"Fork":["Bifurcación","Bifurcaciones"],"ForkedFromProjectPath|Forked from":["Bifurcado de"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Go to your fork":["Ir a tu bifurcación"],"GoToYourFork|Fork":["Bifurcación"],"Home":["Inicio"],"Housekeeping successfully started":["Servicio de limpieza iniciado con éxito"],"Import repository":["Importar repositorio"],"Interval Pattern":["Patrón de intervalo"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"LFSStatus|Disabled":["Deshabilitado"],"LFSStatus|Enabled":["Habilitado"],"Last %d day":["Último %d día","Últimos %d días"],"Last Pipeline":["Último Pipeline"],"Last Update":["Última actualización"],"Last commit":["Último cambio"],"Learn more in the":["Más información en la"],"Learn more in the|pipeline schedules documentation":["documentación sobre la programación de pipelines"],"Leave group":["Abandonar grupo"],"Leave project":["Abandonar proyecto"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"MissingSSHKeyWarningLink|add an SSH key":["agregar una clave SSH"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"New Pipeline Schedule":["Nueva Programación del Pipeline"],"New branch":["Nueva rama"],"New directory":["Nuevo directorio"],"New file":["Nuevo archivo"],"New issue":["Nueva incidencia"],"New merge request":["Nueva solicitud de fusión"],"New schedule":["Nueva programación"],"New snippet":["Nuevo fragmento de código"],"New tag":["Nueva etiqueta"],"No repository":["No hay repositorio"],"No schedules":["No hay programaciones"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"Notification events":["Eventos de notificación"],"NotificationEvent|Close issue":["Cerrar incidencia"],"NotificationEvent|Close merge request":["Cerrar solicitud de fusión"],"NotificationEvent|Failed pipeline":["Pipeline fallido"],"NotificationEvent|Merge merge request":["Integrar solicitud de fusión"],"NotificationEvent|New issue":["Nueva incidencia"],"NotificationEvent|New merge request":["Nueva solicitud de fusión"],"NotificationEvent|New note":["Nueva nota"],"NotificationEvent|Reassign issue":["Reasignar incidencia"],"NotificationEvent|Reassign merge request":["Reasignar solicitud de fusión"],"NotificationEvent|Reopen issue":["Reabrir incidencia"],"NotificationEvent|Successful pipeline":["Pipeline exitoso"],"NotificationLevel|Custom":["Personalizado"],"NotificationLevel|Disabled":["Deshabilitado"],"NotificationLevel|Global":["Global"],"NotificationLevel|On mention":["Cuando me mencionan"],"NotificationLevel|Participate":["Participación"],"NotificationLevel|Watch":["Vigilancia"],"OfSearchInADropdown|Filter":["Filtrar"],"OpenedNDaysAgo|Opened":["Abierto"],"Options":["Opciones"],"Owner":["Propietario"],"Pipeline":["Pipeline"],"Pipeline Health":["Estado del Pipeline"],"Pipeline Schedule":["Programación del Pipeline"],"Pipeline Schedules":["Programaciones de los Pipelines"],"PipelineSchedules|Activated":["Activado"],"PipelineSchedules|Active":["Activos"],"PipelineSchedules|All":["Todos"],"PipelineSchedules|Inactive":["Inactivos"],"PipelineSchedules|Next Run":["Próxima Ejecución"],"PipelineSchedules|None":["Ninguno"],"PipelineSchedules|Provide a short description for this pipeline":["Proporcione una breve descripción para este pipeline"],"PipelineSchedules|Take ownership":["Tomar posesión"],"PipelineSchedules|Target":["Destino"],"PipelineSheduleIntervalPattern|Custom":["Personalizado"],"Pipeline|with stage":["con etapa"],"Pipeline|with stages":["con etapas"],"Project '%{project_name}' queued for deletion.":["Proyecto ‘%{project_name}’ en cola para eliminación."],"Project '%{project_name}' was successfully created.":["Proyecto ‘%{project_name}’ fue creado satisfactoriamente."],"Project '%{project_name}' was successfully updated.":["Proyecto ‘%{project_name}’ fue actualizado satisfactoriamente."],"Project '%{project_name}' will be deleted.":["Proyecto ‘%{project_name}’ será eliminado."],"Project access must be granted explicitly to each user.":["El acceso al proyecto debe concederse explícitamente a cada usuario."],"Project export could not be deleted.":["No se pudo eliminar la exportación del proyecto."],"Project export has been deleted.":["La exportación del proyecto ha sido eliminada."],"Project export link has expired. Please generate a new export from your project settings.":["El enlace de exportación del proyecto ha caducado. Por favor, genera una nueva exportación desde la configuración del proyecto."],"Project export started. A download link will be sent by email.":["Se inició la exportación del proyecto. Se enviará un enlace de descarga por correo electrónico."],"Project home":["Inicio del proyecto"],"ProjectFeature|Disabled":["Deshabilitada"],"ProjectFeature|Everyone with access":["Todos con acceso"],"ProjectFeature|Only team members":["Solo miembros del equipo"],"ProjectFileTree|Name":["Nombre"],"ProjectLastActivity|Never":["Nunca"],"ProjectLifecycle|Stage":["Etapa"],"ProjectNetworkGraph|Graph":["Historial gráfico"],"Read more":["Leer más"],"Readme":["Léeme"],"RefSwitcher|Branches":["Ramas"],"RefSwitcher|Tags":["Etiquetas"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Remind later":["Recordar después"],"Remove project":["Eliminar proyecto"],"Request Access":["Solicitar acceso"],"Revert this commit":["Revertir este cambio"],"Revert this merge request":["Revertir esta solicitud de fusión"],"Save pipeline schedule":["Guardar programación del pipeline"],"Schedule a new pipeline":["Programar un nuevo pipeline"],"Scheduling Pipelines":["Programación de Pipelines"],"Search branches and tags":["Buscar ramas y etiquetas"],"Select Archive Format":["Seleccionar formato de archivo"],"Select a timezone":["Selecciona una zona horaria"],"Select target branch":["Selecciona una rama de destino"],"Set a password on your account to pull or push via %{protocol}":["Establezca una contraseña en su cuenta para actualizar o enviar a través de %{protocol}"],"Set up CI":["Configurar CI"],"Set up Koding":["Configurar Koding"],"Set up auto deploy":["Configurar auto despliegue"],"SetPasswordToCloneLink|set a password":["establecer una contraseña"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"Source code":["Código fuente"],"StarProject|Star":["Destacar"],"Start a %{new_merge_request} with these changes":["Iniciar una %{new_merge_request} con estos cambios"],"Switch branch/tag":["Cambiar rama/etiqueta"],"Tag":["Etiqueta","Etiquetas"],"Tags":["Etiquetas"],"Target Branch":["Rama de destino"],"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.":["La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The fork relationship has been removed.":["La relación con la bifurcación se ha eliminado."],"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.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user.":["La programación de pipelines ejecuta pipelines en el futuro, repetidamente, para ramas o etiquetas específicas. Los pipelines programados heredarán acceso limitado al proyecto basado en su usuario asociado."],"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.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"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.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The project can be accessed by any logged in user.":["El proyecto puede ser accedido por cualquier usuario conectado."],"The project can be accessed without any authentication.":["El proyecto puede accederse sin ninguna autenticación."],"The repository for this project does not exist.":["El repositorio para este proyecto no existe."],"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.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"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.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"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.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"This means you can not push code until you create an empty repository or import existing one.":["Esto significa que no puede enviar código hasta que cree un repositorio vacío o importe uno existente."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Timeago|%s days ago":["hace %s días"],"Timeago|%s days remaining":["%s días restantes"],"Timeago|%s hours remaining":["%s horas restantes"],"Timeago|%s minutes ago":["hace %s minutos"],"Timeago|%s minutes remaining":["%s minutos restantes"],"Timeago|%s months ago":["hace %s meses"],"Timeago|%s months remaining":["%s meses restantes"],"Timeago|%s seconds remaining":["%s segundos restantes"],"Timeago|%s weeks ago":["hace %s semanas"],"Timeago|%s weeks remaining":["%s semanas restantes"],"Timeago|%s years ago":["hace %s años"],"Timeago|%s years remaining":["%s años restantes"],"Timeago|1 day remaining":["1 día restante"],"Timeago|1 hour remaining":["1 hora restante"],"Timeago|1 minute remaining":["1 minuto restante"],"Timeago|1 month remaining":["1 mes restante"],"Timeago|1 week remaining":["1 semana restante"],"Timeago|1 year remaining":["1 año restante"],"Timeago|Past due":["Atrasado"],"Timeago|a day ago":["hace un día"],"Timeago|a month ago":["hace un mes"],"Timeago|a week ago":["hace una semana"],"Timeago|a while":["hace un momento"],"Timeago|a year ago":["hace un año"],"Timeago|about %s hours ago":["hace alrededor de %s horas"],"Timeago|about a minute ago":["hace alrededor de 1 minuto"],"Timeago|about an hour ago":["hace alrededor de 1 hora"],"Timeago|in %s days":["en %s días"],"Timeago|in %s hours":["en %s horas"],"Timeago|in %s minutes":["en %s minutos"],"Timeago|in %s months":["en %s meses"],"Timeago|in %s seconds":["en %s segundos"],"Timeago|in %s weeks":["en %s semanas"],"Timeago|in %s years":["en %s años"],"Timeago|in 1 day":["en 1 día"],"Timeago|in 1 hour":["en 1 hora"],"Timeago|in 1 minute":["en 1 minuto"],"Timeago|in 1 month":["en 1 mes"],"Timeago|in 1 week":["en 1 semana"],"Timeago|in 1 year":["en 1 año"],"Timeago|less than a minute ago":["hace menos de 1 minuto"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Unstar":["No Destacar"],"Upload New File":["Subir nuevo archivo"],"Upload file":["Subir archivo"],"Use your global notification setting":["Utiliza tu configuración de notificación global"],"VisibilityLevel|Internal":["Interno"],"VisibilityLevel|Private":["Privado"],"VisibilityLevel|Public":["Público"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"Withdraw Access Request":["Retirar Solicitud de Acceso"],"You are going to remove %{project_name_with_namespace}.\\nRemoved project CANNOT be restored!\\nAre you ABSOLUTELY sure?":["Va a eliminar %{project_name_with_namespace}.\\n¡El proyecto eliminado NO puede ser restaurado!\\n¿Estás TOTALMENTE seguro?"],"You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?":["Vas a eliminar el enlace de la bifurcación con el proyecto original %{forked_from_project}. ¿Estás TOTALMENTE seguro?"],"You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?":["Vas a transferir %{project_name_with_namespace} a otro propietario. ¿Estás TOTALMENTE seguro?"],"You can only add files when you are on a branch":["Solo puedes agregar archivos cuando estás en una rama"],"You must sign in to star a project":["Debes iniciar sesión para destacar un proyecto"],"You need permission.":["Necesitas permisos."],"You will not get any notifications via email":["No recibirás ninguna notificación por correo electrónico"],"You will only receive notifications for the events you choose":["Solo recibirás notificaciones de los eventos que elijas"],"You will only receive notifications for threads you have participated in":["Solo recibirás notificaciones de los temas en los que has participado"],"You will receive notifications for any activity":["Recibirás notificaciones por cualquier actividad"],"You will receive notifications only for comments in which you were @mentioned":["Recibirás notificaciones solo para los comentarios en los que se te mencionó"],"You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account":["No podrás actualizar o enviar código al proyecto a través de %{protocol} hasta que %{set_password_link} en tu cuenta"],"You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile":["No podrás actualizar o enviar código al proyecto a través de SSH hasta que %{add_ssh_key_link} en su perfil"],"Your name":["Tu nombre"],"day":["día","días"],"new merge request":["nueva solicitud de fusión"],"notification emails":["correos electrónicos de notificación"],"parent":["padre","padres"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/fr/app.js b/app/assets/javascripts/locale/fr/app.js deleted file mode 100644 index f9904ea61ea..00000000000 --- a/app/assets/javascripts/locale/fr/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['fr'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-06-15 20:38+0000","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-14 04:21-0400","Last-Translator":"Dremor <egeorget@opmbx.org>","Language-Team":"French (https://www.transifex.com/gitlab-fr/teams/75145/fr/)","Language":"fr","Plural-Forms":"nplurals=2; plural=(n > 1);","X-Generator":"Zanata 3.9.6","lang":"fr","domain":"app","plural_forms":"nplurals=2; plural=(n > 1);"},"ByAuthor|by":["par"],"Commit":["Validation","Validations"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["L’analyseur de cycle permet d’avoir une vue d’ensemble du temps nécessaire pour aller d’une idée à sa mise en production pour votre projet."],"CycleAnalyticsStage|Code":["Code"],"CycleAnalyticsStage|Issue":["Incident"],"CycleAnalyticsStage|Plan":["Planification"],"CycleAnalyticsStage|Production":["Production"],"CycleAnalyticsStage|Review":["Examen"],"CycleAnalyticsStage|Staging":["Pré-production"],"CycleAnalyticsStage|Test":["Test"],"Deploy":["Déploiement","Déploiements"],"FirstPushedBy|First":["En premier"],"FirstPushedBy|pushed by":["poussé par"],"From issue creation until deploy to production":["Depuis la création de l'incident jusqu'au déploiement en production"],"From merge request merge until deploy to production":["Depuis la fusion de la demande de fusion jusqu'au déploiement en production"],"Introducing Cycle Analytics":["Introduction à l'analyseur de cycle"],"Last %d day":["Le dernier %d jour","Les derniers %d jours"],"Limited to showing %d event at most":["Limiter l'affichage au plus à %d évènement","Limiter l'affichage au plus à %d évènements"],"Median":["Médian"],"New Issue":["Nouvel incident","Nouveaux incidents"],"Not available":["Indisponible"],"Not enough data":["Données insuffisantes"],"OpenedNDaysAgo|Opened":["Ouvert"],"Pipeline Health":["Santé du Pipeline"],"ProjectLifecycle|Stage":["Étape"],"Read more":["Lire plus"],"Related Commits":["Validations liés"],"Related Deployed Jobs":["Tâches de déploiement liés"],"Related Issues":["Incidents liés"],"Related Jobs":["Tâches liées"],"Related Merge Requests":["Demandes de fusion liées"],"Related Merged Requests":["Demandes fusionnées liées"],"Showing %d event":["Affichage de %d évènement","Affichage de %d évènements"],"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.":["L’étape de développement montre le temps entre la première validation et la création de la demande de fusion. Les données seront automatiquement ajoutées ici une fois que vous aurez créé votre première demande de fusion."],"The collection of events added to the data gathered for that stage.":["L’ensemble d’évènements ajoutés aux données récupérées pour cette étape."],"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.":["L'étape des incidents montre le temps nécessaire entre la création d'un incident et son assignation à un jalon, ou son ajout à une liste d'un tableau d'incident. Débutez à créer des incidents pour voir des données pour cette étape."],"The phase of the development lifecycle.":["Les étapes du cycle de développement."],"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.":["L’étape de planification montre le temps entre l’étape précédente et l’envoi de votre première validation. Ce temps sera automatiquement ajouté quand vous pousserez votre première validation."],"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.":["L’étape de mise en production montre le temps nécessaire entre la création d’un incident et le déploiement du code en production. Les données seront automatiquement ajoutées une fois que vous aurez complété le cycle complet, depuis l’idée jusqu’à la mise en production."],"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.":["L’étape d’évaluation montre le temps entre la création de la demande de fusion et la fusion effective de celle-ci. Ces données seront automatiquement ajoutées après que vous ayez fusionné votre première demande de fusion."],"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.":["L’étape de pré-production indique le temps entre la fusion de la RF et le déploiement du code dans l’environnent de production. Les données seront automatiquement ajoutées une fois que vous déploierez en production pour la première fois."],"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.":["L’étape de test montre le temps que le CI de GitLab met pour exécuter chaque pipeline liés à la demande de fusion. Les données seront automatiquement ajoutées après que votre premier pipeline s’achèvera."],"The time taken by each data entry gathered by that stage.":["Le temps pris par chaque entrée récoltée durant cette étape."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["La valeur située au point médian d’une série de valeur observée. C.à.d., entre 3, 5, 9, le médian est 5. Entre 3, 5, 7, 8, le médian est (5+7)/2 = 6."],"Time before an issue gets scheduled":["Temps avant qu’un incident ne soit planifié"],"Time before an issue starts implementation":["Temps avant que résolution ne débute"],"Time between merge request creation and merge/close":["Temps entre la création d'une demande de fusion et sa fusion/clôture"],"Time until first merge request":["Temps jusqu’à la première demande de fusion"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Temps total"],"Total test time for all commits/merges":["Temps total de test pour toutes les validations/fusions"],"Want to see the data? Please ask an administrator for access.":["Vous voulez voir les données ? Merci de contacter un administrateur pour en obtenir l’accès."],"We don't have enough data to show this stage.":["Nous n'avons pas suffisamment de données pour afficher cette étape."],"You need permission.":["Vous avez besoin d’une autorisation."],"day":["jour","jours"],"%{commit_author_link} committed %{commit_timeago}":["%{commit_author_link} a validé %{commit_timeago}"],"About auto deploy":["A propos de l'auto-déploiement"],"Active":["Actif"],"Activity":["Activité"],"Add Changelog":["Ajouter un journal des modifications"],"Add Contribution guide":["Ajouter un guide de contribution"],"Add License":["Ajouter une licence"],"Add an SSH key to your profile to pull or push via SSH.":["Ajoutez une clef SSH à votre profil pour pouvoir récupérer et pousser par SSH."],"Add new directory":["Ajouter un nouveau dossier"],"Archived project! Repository is read-only":["Projet archivé ! Le dépôt est en lecture seule"],"Are you sure you want to delete this pipeline schedule?":["Êtes-vous sûr de vouloir supprimer ce pipeline programmé"],"Attach a file by drag & drop or %{upload_link}":["Attachez un fichier par glisser & déposer ou %{upload_link}"],"Branch":["Branche","Branches"],"#~ \"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, cho\"#~ \"ose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_do\"#~ \"c}\"":["#~ \"La branche <strong>%{branch_name}</strong> a été crée. Pour mettre en place le\"#~ \" déploiement automatisé, sélectionnez un modèle de fichier Yaml pour Gitlab CI\"#~ \", et validez les modifications. %{link_to_autodeploy_doc}\""],"Branches":["Branches"],"Browse files":["Parcourir les fichiers"],"CI configuration":["Configuration du CI"],"Cancel":["Annuler"],"ChangeTypeActionLabel|Pick into branch":["Sélectionner dans la branche"],"ChangeTypeActionLabel|Revert in branch":["Annuler dans la branche"],"ChangeTypeAction|Cherry-pick":["Sélectionner"],"ChangeType|commit":["validation"],"ChangeType|merge request":["demande de fusion"],"Changelog":["Journal des modifications"],"Charts":["Graphiques"],"Cherry-pick this commit":["Sélectionner cette validation"],"Cherry-pick this merge-request":["Sélectionner cette demande de fusion"],"CiStatusLabel|canceled":["annulé"],"CiStatusLabel|created":["créé"],"CiStatusLabel|failed":["échoué"],"CiStatusLabel|manual action":["action manuelle"],"CiStatusLabel|passed":["passé"],"CiStatusLabel|passed with warnings":["passé avec des avertissements"],"CiStatusLabel|pending":["en attente"],"CiStatusLabel|skipped":["ignoré"],"CiStatusLabel|waiting for manual action":["en attente d'action manuelle"],"CiStatusText|blocked":["bloqué"],"CiStatusText|canceled":["annulé "],"CiStatusText|created":["créé"],"CiStatusText|failed":["échoué"],"CiStatusText|manual":["manuel"],"CiStatusText|passed":["passé"],"CiStatusText|pending":["en attente"],"CiStatusText|skipped":["ignoré"],"CiStatus|running":["en cours"],"Commit message":["Message de validation"],"CommitMessage|Add %{file_name}":["Ajout de %{file_name}"],"Commits":["Validations"],"Commits|History":["Historique"],"Committed by":["Validé par"],"Compare":["Comparer"],"Contribution guide":["Guilde de contribution"],"Contributors":["Contributeurs"],"Copy URL to clipboard":["Copier l'URL dans le presse-papier"],"Copy commit SHA to clipboard":["Copier le SAH de la validation"],"Create New Directory":["Créer un nouveau dossier"],"Create directory":["Créer un dossier"],"Create empty bare repository":["Créer un dépôt vide"],"Create merge request":["Créer une demande de fusion"],"Create new...":["Créer nouveau..."],"CreateNewFork|Fork":["Fork"],"CreateTag|Tag":["Étiquette"],"Cron Timezone":["Fuseau horaire de Cron"],"Cron syntax":["Syntaxe CRON"],"Custom":["Personnalisé"],"Custom notification events":["Événements de notification personnalisés"],"#~ \"Custom notification levels are the same as participating levels. With custom n\"#~ \"otification levels you will also receive notifications for select events. To f\"#~ \"ind out more, check out %{notification_link}.\"":["#~ \"Le niveau de notification Personnalisé est similaire au niveau Participation. \"#~ \"Il permet cependant également de recevoir des notifications pour des événement\"#~ \"s sélectionnés. Pour plus d’information, vous pouvez consulter %{notification_\"#~ \"link}.\""],"Cycle Analytics":["Analyseur de cycle"],"Define a custom pattern with cron syntax":["Définir un schéma personnalisé avec une syntaxe CRON"],"Delete":["Supprimer"],"Description":["Description"],"Directory name":["Nom du dossier"],"Don't show again":["Ne plus montrer"],"Download":["Télécharger"],"Download tar":["Télécharger tar"],"Download tar.bz2":["Télécharger tar.bz2"],"Download tar.gz":["Télécharger tar.gz"],"Download zip":["Télécharger zip"],"DownloadArtifacts|Download":["Télécharger"],"DownloadCommit|Email Patches":["Patch email"],"DownloadCommit|Plain Diff":["Diff simple"],"DownloadSource|Download":["Télécharger"],"Edit":["Éditer"],"Edit Pipeline Schedule %{id}":["Éditer le pipeline programmé %{id}"],"Every day (at 4:00am)":["Chaque jour (à 4:00 du matin)"],"Every month (on the 1st at 4:00am)":["Chaque mois (le 1er à 4:00 du matin)"],"Every week (Sundays at 4:00am)":["Chaque semaine (Dimanche à 4:00 du matin)"],"Failed to change the owner":["Échec du changement de propriétaire"],"Failed to remove the pipeline schedule":["Échec de la suppression du pipeline programmé"],"Files":["Fichiers"],"Find by path":["Rechercher par chemin"],"Find file":["Rechercher un fichier"],"Fork":["Fork","Forks"],"ForkedFromProjectPath|Forked from":["Forké depuis"],"Go to your fork":["Aller à votre fork"],"GoToYourFork|Fork":["Fork"],"Home":["Accueil"],"Housekeeping successfully started":["Maintenance démarrée avec succès"],"Import repository":["Importer un dépôt"],"Interval Pattern":["Schéma d’intervalle"],"LFSStatus|Disabled":["Désactivé"],"LFSStatus|Enabled":["Activé"],"Last Pipeline":["Dernier pipeline"],"Last Update":["Dernière mise à jour"],"Last commit":["Dernière validation"],"Learn more in the":["En apprendre plus dans le"],"Leave group":["Quitter le groupe"],"Leave project":["Quitter le projet"],"MissingSSHKeyWarningLink|add an SSH key":["ajouter un clef SSH"],"New Pipeline Schedule":["Nouveau pipeline programmé"],"New branch":["Nouvelle branche"],"New directory":["Nouveau dossier"],"New file":["Nouveau Fichier"],"New issue":["Nouvel incident"],"New merge request":["Nouvelle demande de fusion"],"New schedule":["Nouveau programme"],"New snippet":["Nouvel extrait de code"],"New tag":["Nouvelle étiquette"],"No repository":["Pas de dépôt"],"No schedules":["Aucun programme"],"Notification events":["Événement de notifications"],"NotificationEvent|Close issue":["Clore l'incident"],"NotificationEvent|Close merge request":["Clore la demande de fusion"],"NotificationEvent|Failed pipeline":["Pipeline échoué"],"NotificationEvent|Merge merge request":["Fusionner le demande de fusion"],"NotificationEvent|New issue":["Nouvel incident"],"NotificationEvent|New merge request":["Nouvelle demande de fusion"],"NotificationEvent|New note":["Nouvelle note"],"NotificationEvent|Reassign issue":["Réassigner l'incident"],"NotificationEvent|Reassign merge request":["Réassigner la demande de fusion"],"NotificationEvent|Reopen issue":["Ré-ouvrir l'incident"],"NotificationEvent|Successful pipeline":["Pipeline réussi"],"NotificationLevel|Custom":["Personnalisé"],"NotificationLevel|Disabled":["Désactivé"],"NotificationLevel|Global":["Global"],"NotificationLevel|On mention":["En cas de mention"],"NotificationLevel|Participate":["Participation"],"NotificationLevel|Watch":["Surveillé"],"OfSearchInADropdown|Filter":["Filtre"],"Options":["Options"],"Owner":["Propriétaire"],"Pipeline":["Pipeline"],"Pipeline Schedule":["Programmation de pipeline"],"Pipeline Schedules":["Programmations de pipeline"],"PipelineSchedules|Activated":["Activé"],"PipelineSchedules|Active":["Active"],"PipelineSchedules|All":["Tous"],"PipelineSchedules|Inactive":["Inactive"],"PipelineSchedules|Next Run":["Prochaine exécution"],"PipelineSchedules|None":["Aucune"],"PipelineSchedules|Provide a short description for this pipeline":["Indiquez une courte description"],"PipelineSchedules|Take ownership":["S’approprier"],"PipelineSchedules|Target":["Cible"],"Project '%{project_name}' queued for deletion.":["Projet '%{project_name}' en attente de suppression."],"Project '%{project_name}' was successfully created.":["Projet '%{project_name}' créé avec succès."],"Project '%{project_name}' was successfully updated.":["Projet '%{project_name}' mis à jour avec succès."],"Project '%{project_name}' will be deleted.":["Projet '%{project_name}' sera supprimé."],"Project access must be granted explicitly to each user.":["L’accès au projet doit être explicitement accordé à chaque utilisateur."],"Project export could not be deleted.":["L'export du projet n'a pas pu être supprimé."],"Project export has been deleted.":["L'export du projet a été supprimé."],"#~ \"Project export link has expired. Please generate a new export from your projec\"#~ \"t settings.\"":["#~ \"Le lien de l’export du projet a expiré. Merci de générer un nouvel export depu\"#~ \"is les paramètres du projet.\""],"Project export started. A download link will be sent by email.":["#~ \"L'export du projet a débuté. Un lien de téléchargement sera envoyé par courrie\"#~ \"l.\""],"Project home":["Accueil du projet"],"ProjectFeature|Disabled":["Désactivé"],"ProjectFeature|Everyone with access":["Toute personne ayant accès"],"ProjectFeature|Only team members":["Seulement les membres de l'équipe"],"ProjectFileTree|Name":["Nom"],"ProjectLastActivity|Never":["Jamais"],"ProjectNetworkGraph|Graph":["Graphique "],"Readme":["LisezMoi"],"RefSwitcher|Branches":["Branches"],"RefSwitcher|Tags":["Étiquettes"],"Remind later":["Me le rappeler ultérieurement"],"Remove project":["Supprimer le projet"],"Request Access":["Demander l'accès"],"Revert this commit":["Annuler cette validation"],"Revert this merge-request":["Annuler cette demande de fusion"],"Save pipeline schedule":["Sauvegarder le pipeline programmé"],"Schedule a new pipeline":["Programmer un nouveau pipeline"],"Scheduling Pipelines":["Programmer des pipelines"],"Search branches and tags":["Rechercher dans les branches et les étiquettes"],"Select Archive Format":["Sélectionnez le format de l'archive"],"Select a timezone":["Sélectionnez un fuseau horaire"],"Select target branch":["Sélectionnez une branche cible"],"Set a password on your account to pull or push via %{protocol}":["#~ \"Définissez un mot de passe pour votre compte pour pouvoir tirer ou pousser par\"#~ \" %{protocol}\""],"Set up CI":["Mettre en place le CI"],"Set up Koding":["Mettre en place Koding"],"Set up auto deploy":["Mettre en place l’auto-déploiement "],"SetPasswordToCloneLink|set a password":["définir un mot de passe"],"Source code":["Code source"],"StarProject|Star":["S'abonner"],"Start a <strong>new merge request</strong> with these changes":["Créer une <strong>nouvelle demande de fusion</strong> avec ces changements"],"Switch branch/tag":["Changer de branche / d'étiquette"],"Tag":["Étiquette","Étiquettes"],"Tags":["Étiquettes"],"Target Branch":["Branche cible"],"The fork relationship has been removed.":["La relation de fork a été supprimée."],"#~ \"The pipelines schedule runs pipelines in the future, repeatedly, for specific \"#~ \"branches or tags. Those scheduled pipelines will inherit limited project acces\"#~ \"s based on their associated user.\"":["#~ \"Les pipelines programmés exécutent des pipelines dans le futur, de façon répét\"#~ \"ée, pour les branches et étiquettes spécifiées. Ces pipelines programmés hérit\"#~ \"ent d’un accès partiel au projet basé sur l’utilisateur que leurs est associé.\""],"The project can be accessed by any logged in user.":["Votre projet peut être accédé par n’importe quel utilisateur authentifié"],"The project can be accessed without any authentication.":["Votre projet peut être accédé sans aucune authentification."],"The repository for this project does not exist.":["Le dépôt pour ce projet n'existe pas."],"#~ \"This means you can not push code until you create an empty repository or impor\"#~ \"t existing one.\"":["#~ \"Cela signifie que vous ne pouvez pas pousser du code tant que vous ne créez pa\"#~ \"s un dépôt vide, ou importez une dépôt existant.\""],"Timeago|%s days ago":["Il y a %s jours"],"Timeago|%s days remaining":["Il reste %s jours"],"Timeago|%s hours remaining":["Il reste %s heures"],"Timeago|%s minutes ago":["Il y a %s minutes"],"Timeago|%s minutes remaining":["Il reste %s minutes"],"Timeago|%s months ago":["Il y a %s mois"],"Timeago|%s months remaining":["Il reste %s mois"],"Timeago|%s seconds remaining":["Il reste %s secondes"],"Timeago|%s weeks ago":["Il y a %s semaines"],"Timeago|%s weeks remaining":["Il reste %s semaines"],"Timeago|%s years ago":["Il y a %s ans"],"Timeago|%s years remaining":["Il reste %s ans"],"Timeago|1 day remaining":["Il reste un jour"],"Timeago|1 hour remaining":["Il reste une heure"],"Timeago|1 minute remaining":["Il reste une minute"],"Timeago|1 month remaining":["Il reste un mois"],"Timeago|1 week remaining":["Il reste une semaine"],"Timeago|1 year remaining":["Il reste un an"],"Timeago|Past due":["En retard"],"Timeago|a day ago":["Il y a un jour"],"Timeago|a month ago":["Il y a un mois"],"Timeago|a week ago":["Il y a une semaine"],"Timeago|a while":["Il y a un moment"],"Timeago|a year ago":["Il y a un an"],"Timeago|about %s hours ago":["Il y a environ %s heures"],"Timeago|about a minute ago":["Il y a environ une minute"],"Timeago|about an hour ago":["Il y a environ une heure"],"Timeago|in %s days":["Dans %s jours"],"Timeago|in %s hours":["Dans %s heures"],"Timeago|in %s minutes":["Dans %s minutes"],"Timeago|in %s months":["Dans %s mois"],"Timeago|in %s seconds":["Dans %s secondes"],"Timeago|in %s weeks":["Dans %s semaines"],"Timeago|in %s years":["Dans %s années"],"Timeago|in 1 day":["Dans 1 jour"],"Timeago|in 1 hour":["Dans 1 heure"],"Timeago|in 1 minute":["Dans 1 minute"],"Timeago|in 1 month":["Dans 1 mois"],"Timeago|in 1 week":["Dans 1 semaine"],"Timeago|in 1 year":["Dans 1 an"],"Timeago|less than a minute ago":["il y a moins d'une minute"],"Unstar":["Se désabonner"],"Upload New File":["Téléverser un nouveau fichier"],"Upload file":["Téléverser un fichier"],"Use your global notification setting":["Utiliser vos paramètres de notification globaux"],"VisibilityLevel|Internal":["Interne"],"VisibilityLevel|Private":["Privé"],"VisibilityLevel|Public":["Publique"],"Withdraw Access Request":["Retirer la demande d'accès"],"#~ \"You are going to remove %{project_name_with_namespace}.\\n\"#~ \"Removed project CANNOT be restored!\\n\"#~ \"Are you ABSOLUTELY sure?\"":["#~ \"Vous êtes sur le point de supprimer %{project_name_with_namespace}.\\n\"#~ \"Les projets supprimés NE PEUVENT PAS être restaurés !\\n\"#~ \"Êtes vous ABSOLUMENT sûr ? \""],"#~ \"You are going to remove the fork relationship to source project %{forked_from_\"#~ \"project}. Are you ABSOLUTELY sure?\"":["#~ \"Vous allez supprimer la relation de fork avec le projet source %{forked_from_p\"#~ \"roject}. Êtes-vous VRAIMENT sûr.\""],"#~ \"You are going to transfer %{project_name_with_namespace} to another owner. Are\"#~ \" you ABSOLUTELY sure?\"":["#~ \"Vous allez transférer %{project_name_with_namespace} à un nouveau propriétaire\"#~ \". Êtes vous VRAIMENT sûr ?\""],"You can only add files when you are on a branch":["Vous ne pouvez ajouter de fichier que dans une branche"],"You must sign in to star a project":["Vous devez vous identifier pour vous abonner à un projet"],"You will not get any notifications via email":["Vous ne recevrez aucune notification par courriel"],"You will only receive notifications for the events you choose":["#~ \"Vous ne recevrez de notification que pour les évènements que vous aurez choisi\"#~ \"s\""],"You will only receive notifications for threads you have participated in":["#~ \"Vous ne recevrez de notification que pour les sujets auxquels vous avez partic\"#~ \"ipé\""],"You will receive notifications for any activity":["Vous recevrez des notifications pour n’importe quelles activités"],"You will receive notifications only for comments in which you were @mentioned":["#~ \"Vous ne recevrez de notifications que pour les commentaires où vous êtes @ment\"#~ \"ionné\""],"#~ \"You won't be able to pull or push project code via %{protocol} until you %{set\"#~ \"_password_link} on your account\"":["#~ \"Vous ne pourrez pas récupérer ou pousser de code par %{protocol} tant que vo\"#~ \"us n'aurez pas %{set_password_link} pour votre compte\""],"#~ \"You won't be able to pull or push project code via SSH until you %{add_ssh_key\"#~ \"_link} to your profile\"":["#~ \"Vous ne pourrez pas récupérer ou pousser de code par SSH tant que vous n’aur\"#~ \"ez pas %{add_ssh_key_link} dans votre profil\""],"Your name":["Votre nom"],"notification emails":["courriels de notification"],"parent":["parent","parents"],"pipeline schedules documentation":["documentation des pipeline programmés"],"with stage":["avec l'étape","avec les étapes"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/pt_BR/app.js b/app/assets/javascripts/locale/pt_BR/app.js deleted file mode 100644 index f2eed3da064..00000000000 --- a/app/assets/javascripts/locale/pt_BR/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['pt_BR'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","POT-Creation-Date":"2017-05-04 19:24-0500","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","PO-Revision-Date":"2017-06-05 03:29-0400","Last-Translator":"Alexandre Alencar <alexandre.alencar@gmail.com>","Language-Team":"Portuguese (Brazil)","Language":"pt-BR","X-Generator":"Zanata 3.9.6","Plural-Forms":"nplurals=2; plural=(n != 1)","lang":"pt_BR","domain":"app","plural_forms":"nplurals=2; plural=(n != 1)"},"ByAuthor|by":["por"],"Commit":["Commit","Commits"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["A Análise de Ciclo fornece uma visão geral de quanto tempo uma ideia demora para ir para produção em seu projeto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Tarefa"],"CycleAnalyticsStage|Plan":["Plano"],"CycleAnalyticsStage|Production":["Produção"],"CycleAnalyticsStage|Review":["Revisão"],"CycleAnalyticsStage|Staging":["Homologação"],"CycleAnalyticsStage|Test":["Teste"],"Deploy":["Implantação","Implantações"],"FirstPushedBy|First":["Primeiro"],"FirstPushedBy|pushed by":["publicado por"],"From issue creation until deploy to production":["Da criação de tarefas até a implantação para a produção"],"From merge request merge until deploy to production":["Da incorporação do merge request até a implantação em produção"],"Introducing Cycle Analytics":["Apresentando a Análise de Ciclo"],"Last %d day":["Último %d dia","Últimos %d dias"],"Limited to showing %d event at most":["Limitado a mostrar %d evento no máximo","Limitado a mostrar %d eventos no máximo"],"Median":["Mediana"],"New Issue":["Nova Tarefa","Novas Tarefas"],"Not available":["Não disponível"],"Not enough data":["Dados insuficientes"],"OpenedNDaysAgo|Opened":["Aberto"],"Pipeline Health":["Saúde da Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Ler mais"],"Related Commits":["Commits Relacionados"],"Related Deployed Jobs":["Jobs Relacionados Incorporados"],"Related Issues":["Tarefas Relacionadas"],"Related Jobs":["Jobs Relacionados"],"Related Merge Requests":["Merge Requests Relacionados"],"Related Merged Requests":["Merge Requests Relacionados"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"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.":["O estágio de codificação mostra o tempo desde o primeiro commit até a criação do merge request. \\nOs dados serão automaticamente adicionados aqui uma vez que você tenha criado seu primeiro merge request."],"The collection of events added to the data gathered for that stage.":["A coleção de eventos adicionados aos dados coletados para esse estágio."],"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.":["O estágio em questão mostra o tempo que leva desde a criação de uma tarefa até a sua assinatura para um milestone, ou a sua adição para a lista no seu Painel de Tarefas. Comece a criar tarefas para ver dados para esta etapa."],"The phase of the development lifecycle.":["A fase do ciclo de vida do desenvolvimento."],"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.":["A fase de planejamento mostra o tempo do passo anterior até empurrar o seu primeiro commit. Este tempo será adicionado automaticamente assim que você realizar seu primeiro commit."],"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.":["O estágio de produção mostra o tempo total que leva entre criar uma tarefa e implantar o código na produção. Os dados serão adicionados automaticamente até que você complete todo o ciclo de produção."],"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.":["A etapa de revisão mostra o tempo de criação de um merge request até que o merge seja feito. Os dados serão automaticamente adicionados depois que você fizer seu primeiro merge request."],"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.":["O estágio de estágio mostra o tempo entre a fusão do MR e o código de implantação para o ambiente de produção. Os dados serão automaticamente adicionados depois de implantar na produção pela primeira vez."],"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.":["A fase de teste mostra o tempo que o GitLab CI leva para executar cada pipeline para o merge request relacionado. Os dados serão automaticamente adicionados após a conclusão do primeiro pipeline."],"The time taken by each data entry gathered by that stage.":["O tempo necessário para cada entrada de dados reunida por essa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["O valor situado no ponto médio de uma série de valores observados. Ex., entre 3, 5, 9, a mediana é 5. Entre 3, 5, 7, 8, a mediana é (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tempo até que uma tarefa seja planejada"],"Time before an issue starts implementation":["Tempo até que uma tarefa comece a ser implementada"],"Time between merge request creation and merge/close":["Tempo entre a criação do merge request e o merge/fechamento"],"Time until first merge request":["Tempo até o primeiro merge request"],"Time|hr":["h","hs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tempo Total"],"Total test time for all commits/merges":["Tempo de teste total para todos os commits/merges"],"Want to see the data? Please ask an administrator for access.":["Precisa visualizar os dados? Solicite acesso ao administrador."],"We don't have enough data to show this stage.":["Não temos dados suficientes para mostrar esta fase."],"You need permission.":["Você precisa de permissão."],"day":["dia","dias"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_CN/app.js b/app/assets/javascripts/locale/zh_CN/app.js deleted file mode 100644 index d1335cfbc0f..00000000000 --- a/app/assets/javascripts/locale/zh_CN/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['zh_CN'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (China) (https://www.transifex.com/gitlab-zh/teams/75177/zh_CN/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_CN","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_CN","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["提交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["周期分析概述了项目从想法到产品实现的各阶段所需的时间。"],"CycleAnalyticsStage|Code":["编码"],"CycleAnalyticsStage|Issue":["议题"],"CycleAnalyticsStage|Plan":["计划"],"CycleAnalyticsStage|Production":["生产"],"CycleAnalyticsStage|Review":["评审"],"CycleAnalyticsStage|Staging":["预发布"],"CycleAnalyticsStage|Test":["测试"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["从创建议题到部署至生产环境"],"From merge request merge until deploy to production":["从合并请求被合并后到部署至生产环境"],"Interval Pattern":[""],"Introducing Cycle Analytics":["周期分析简介"],"Last %d day":["最后 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多显示 %d 个事件"],"Median":["中位数"],"New Issue":["新议题"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["数据不足"],"Not enough data":["数据不足"],"OpenedNDaysAgo|Opened":["开始于"],"Owner":[""],"Pipeline Health":["流水线健康指标"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["项目生命周期"],"Read more":["了解更多"],"Related Commits":["相关的提交"],"Related Deployed Jobs":["相关的部署作业"],"Related Issues":["相关的议题"],"Related Jobs":["相关的作业"],"Related Merge Requests":["相关的合并请求"],"Related Merged Requests":["相关已合并的合并请求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["显示 %d 个事件"],"Target Branch":[""],"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.":["编码阶段概述了从第一次提交到创建合并请求的时间。创建第一个合并请求后,数据将自动添加到此处。"],"The collection of events added to the data gathered for that stage.":["与该阶段相关的事件。"],"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.":["议题阶段概述了从创建议题到将议题设置里程碑或将议题添加到议题看板的时间。开始创建议题以查看此阶段的数据。"],"The phase of the development lifecycle.":["项目生命周期中的各个阶段。"],"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.":["计划阶段概述了从议题添加到日程后到推送首次提交的时间。当首次推送提交后,数据将自动添加到此处。"],"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.":["生产阶段概述了从创建一个议题到将代码部署到生产环境的总时间。当完成想法到部署生产的循环,数据将自动添加到此处。"],"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.":["评审阶段概述了从创建合并请求到被合并的时间。当创建第一个合并请求后,数据将自动添加到此处。"],"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.":["预发布阶段概述了从合并请求被合并到部署至生产环境的总时间。首次部署到生产环境后,数据将自动添加到此处。"],"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.":["测试阶段概述了GitLab CI为相关合并请求运行每个流水线所需的时间。当第一个流水线运行完成后,数据将自动添加到此处。"],"The time taken by each data entry gathered by that stage.":["该阶段每条数据所花的时间"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位数是一个数列中最中间的值。例如在 3、5、9 之间,中位数是 5。在 3、5、7、8 之间,中位数是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["议题被列入日程表的时间"],"Time before an issue starts implementation":["开始进行编码前的时间"],"Time between merge request creation and merge/close":["从创建合并请求到被合并或关闭的时间"],"Time until first merge request":["创建第一个合并请求之前的时间"],"Time|hr":["小时"],"Time|min":["分钟"],"Time|s":["秒"],"Total Time":["总时间"],"Total test time for all commits/merges":["所有提交和合并的总测试时间"],"Want to see the data? Please ask an administrator for access.":["权限不足。如需查看相关数据,请向管理员申请权限。"],"We don't have enough data to show this stage.":["该阶段的数据不足,无法显示。"],"You need permission.":["您需要相关的权限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_HK/app.js b/app/assets/javascripts/locale/zh_HK/app.js deleted file mode 100644 index 30cb1e6b89e..00000000000 --- a/app/assets/javascripts/locale/zh_HK/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['zh_HK'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Hong Kong) (https://www.transifex.com/gitlab-zh/teams/75177/zh_HK/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_HK","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_HK","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["提交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了項目從想法到產品實現的各階段所需的時間。"],"CycleAnalyticsStage|Code":["編碼"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["生產"],"CycleAnalyticsStage|Review":["評審"],"CycleAnalyticsStage|Staging":["預發布"],"CycleAnalyticsStage|Test":["測試"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從創建議題到部署到生產環境"],"From merge request merge until deploy to production":["從合併請求的合併到部署至生產環境"],"Interval Pattern":[""],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["不可用"],"Not enough data":["數據不足"],"OpenedNDaysAgo|Opened":["開始於"],"Owner":[""],"Pipeline Health":["流水線健康指標"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["項目生命週期"],"Read more":["了解更多"],"Related Commits":["相關的提交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的合並請求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["顯示 %d 個事件"],"Target Branch":[""],"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.":["編碼階段概述了從第一次提交到創建合併請求的時間。創建第壹個合並請求後,數據將自動添加到此處。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"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.":["議題階段概述了從創建議題到將議題設置裏程碑或將議題添加到議題看板的時間。創建一個議題後,數據將自動添加到此處。"],"The phase of the development lifecycle.":["項目生命週期中的各個階段。"],"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.":["計劃階段概述了從議題添加到日程後到推送首次提交的時間。當首次推送提交後,數據將自動添加到此處。"],"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.":["生產階段概述了從創建議題到將代碼部署到生產環境的時間。當完成完整的想法到部署生產,數據將自動添加到此處。"],"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.":["評審階段概述了從創建合並請求到合併的時間。當創建第壹個合並請求後,數據將自動添加到此處。"],"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.":["預發布階段概述了合並請求的合併到部署代碼到生產環境的總時間。當首次部署到生產環境後,數據將自動添加到此處。"],"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.":["測試階段概述了GitLab CI為相關合併請求運行每個流水線所需的時間。當第壹個流水線運行完成後,數據將自動添加到此處。"],"The time taken by each data entry gathered by that stage.":["該階段每條數據所花的時間"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["開始進行編碼前的時間"],"Time between merge request creation and merge/close":["從創建合併請求到被合並或關閉的時間"],"Time until first merge request":["創建第壹個合併請求之前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有提交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關數據,請向管理員申請權限。"],"We don't have enough data to show this stage.":["該階段的數據不足,無法顯示。"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/locale/zh_TW/app.js b/app/assets/javascripts/locale/zh_TW/app.js deleted file mode 100644 index f0fe1e31f18..00000000000 --- a/app/assets/javascripts/locale/zh_TW/app.js +++ /dev/null @@ -1 +0,0 @@ -var locales = locales || {}; locales['zh_TW'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Last-Translator":"HuangTao <htve@outlook.com>, 2017","Language-Team":"Chinese (Taiwan) (https://www.transifex.com/gitlab-zh/teams/75177/zh_TW/)","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Language":"zh_TW","Plural-Forms":"nplurals=1; plural=0;","lang":"zh_TW","domain":"app","plural_forms":"nplurals=1; plural=0;"},"Are you sure you want to delete this pipeline schedule?":[""],"ByAuthor|by":["作者:"],"Cancel":[""],"Commit":["送交"],"Cron Timezone":[""],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["週期分析概述了你的專案從想法到產品實現,各階段所需的時間。"],"CycleAnalyticsStage|Code":["程式開發"],"CycleAnalyticsStage|Issue":["議題"],"CycleAnalyticsStage|Plan":["計劃"],"CycleAnalyticsStage|Production":["上線"],"CycleAnalyticsStage|Review":["複閱"],"CycleAnalyticsStage|Staging":["預備"],"CycleAnalyticsStage|Test":["測試"],"Delete":[""],"Deploy":["部署"],"Description":[""],"Edit":[""],"Edit Pipeline Schedule %{id}":[""],"Failed to change the owner":[""],"Failed to remove the pipeline schedule":[""],"Filter":[""],"FirstPushedBy|First":["首次推送"],"FirstPushedBy|pushed by":["推送者:"],"From issue creation until deploy to production":["從議題建立至線上部署"],"From merge request merge until deploy to production":["從請求被合併後至線上部署"],"Interval Pattern":[""],"Introducing Cycle Analytics":["週期分析簡介"],"Last %d day":["最後 %d 天"],"Last Pipeline":[""],"Limited to showing %d event at most":["最多顯示 %d 個事件"],"Median":["中位數"],"New Issue":["新議題"],"New Pipeline Schedule":[""],"No schedules":[""],"Not available":["無法使用"],"Not enough data":["資料不足"],"OpenedNDaysAgo|Opened":["開始於"],"Owner":[""],"Pipeline Health":["流水線健康指標"],"Pipeline Schedule":[""],"Pipeline Schedules":[""],"PipelineSchedules|Activated":[""],"PipelineSchedules|Active":[""],"PipelineSchedules|All":[""],"PipelineSchedules|Inactive":[""],"PipelineSchedules|Next Run":[""],"PipelineSchedules|None":[""],"PipelineSchedules|Provide a short description for this pipeline":[""],"PipelineSchedules|Take ownership":[""],"PipelineSchedules|Target":[""],"ProjectLifecycle|Stage":["專案生命週期"],"Read more":["了解更多"],"Related Commits":["相關的送交"],"Related Deployed Jobs":["相關的部署作業"],"Related Issues":["相關的議題"],"Related Jobs":["相關的作業"],"Related Merge Requests":["相關的合併請求"],"Related Merged Requests":["相關已合併的請求"],"Save pipeline schedule":[""],"Schedule a new pipeline":[""],"Select a timezone":[""],"Select target branch":[""],"Showing %d event":["顯示 %d 個事件"],"Target Branch":[""],"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.":["程式開發階段顯示從第一次送交到建立合併請求的時間。建立第一個合併請求後,資料將自動填入。"],"The collection of events added to the data gathered for that stage.":["與該階段相關的事件。"],"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.":["議題階段顯示從議題建立到設置里程碑、或將該議題加至議題看板的時間。建立第一個議題後,資料將自動填入。"],"The phase of the development lifecycle.":["專案開發生命週期的各個階段。"],"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.":["計劃階段顯示從議題添加到日程後至推送第一個送交的時間。當第一次推送送交後,資料將自動填入。"],"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.":["上線階段顯示從建立一個議題到部署程式至線上的總時間。當完成從想法到產品實現的循環後,資料將自動填入。"],"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.":["複閱階段顯示從合併請求建立後至被合併的時間。當建立第一個合併請求後,資料將自動填入。"],"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.":["預備階段顯示從合併請求被合併後至部署上線的時間。當第一次部署上線後,資料將自動填入。"],"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.":["測試階段顯示相關合併請求的流水線所花的時間。當第一個流水線運作完畢後,資料將自動填入。"],"The time taken by each data entry gathered by that stage.":["每筆該階段相關資料所花的時間。"],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["中位數是一個數列中最中間的值。例如在 3、5、9 之間,中位數是 5。在 3、5、7、8 之間,中位數是 (5 + 7)/ 2 = 6。"],"Time before an issue gets scheduled":["議題被列入日程表的時間"],"Time before an issue starts implementation":["議題等待開始實作的時間"],"Time between merge request creation and merge/close":["合併請求被合併或是關閉的時間"],"Time until first merge request":["第一個合併請求被建立前的時間"],"Time|hr":["小時"],"Time|min":["分鐘"],"Time|s":["秒"],"Total Time":["總時間"],"Total test time for all commits/merges":["所有送交和合併的總測試時間"],"Want to see the data? Please ask an administrator for access.":["權限不足。如需查看相關資料,請向管理員申請權限。"],"We don't have enough data to show this stage.":["因該階段的資料不足而無法顯示相關資訊"],"You need permission.":["您需要相關的權限。"],"day":["天"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index ed7629948ca..cd45091c211 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -70,7 +70,7 @@ import './ajax_loading_spinner'; import './api'; import './aside'; import './autosave'; -import AwardsHandler from './awards_handler'; +import loadAwardsHandler from './awards_handler'; import './breakpoints'; import './broadcast_message'; import './build'; @@ -109,6 +109,7 @@ import './label_manager'; import './labels'; import './labels_select'; import './layout_nav'; +import LazyLoader from './lazy_loader'; import './line_highlighter'; import './logo'; import './member_expiration_date'; @@ -143,26 +144,11 @@ import './render_math'; import './right_sidebar'; import './search'; import './search_autocomplete'; -import './signin_tabs_memoizer'; -import './single_file_diff'; import './smart_interval'; -import './snippets_list'; import './star'; import './subscription'; import './subscription_select'; import './syntax_highlight'; -import './task_list'; -import './todos'; -import './tree'; -import './usage_ping'; -import './user'; -import './user_tabs'; -import './username_validator'; -import './users_select'; -import './version_check_image'; -import './visibility_select'; -import './wikis'; -import './zen_mode'; // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); @@ -172,6 +158,8 @@ document.addEventListener('beforeunload', function () { $(document).off('scroll'); // Close any open tooltips $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy'); + // Close any open popover + $('[data-toggle="popover"]').popover('destroy'); }); window.addEventListener('hashchange', gl.utils.handleLocationHash); @@ -180,6 +168,11 @@ window.addEventListener('load', function onLoad() { gl.utils.handleLocationHash(); }, false); +gl.lazyLoader = new LazyLoader({ + scrollContainer: window, + observerNode: '#content-body' +}); + $(function () { var $body = $('body'); var $document = $(document); @@ -255,6 +248,11 @@ $(function () { return $(el).data('placement') || 'bottom'; } }); + // Initialize popovers + $body.popover({ + selector: '[data-toggle="popover"]', + trigger: 'focus' + }); $('.trigger-submit').on('change', function () { return $(this).parents('form').submit(); // Form submitter @@ -298,12 +296,7 @@ $(function () { return $container.remove(); // Commit show suppressed diff }); - $('.navbar-toggle').on('click', function () { - $('.header-content .title').toggle(); - $('.header-content .header-logo').toggle(); - $('.header-content .navbar-collapse').toggle(); - return $('.navbar-toggle').toggleClass('active'); - }); + $('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded')); // Show/hide comments on diff $body.on('click', '.js-toggle-diff-comments', function (e) { var $this = $(this); @@ -354,10 +347,20 @@ $(function () { $window.off('resize.app').on('resize.app', function () { return fitSidebarForSize(); }); - gl.awardsHandler = new AwardsHandler(); + loadAwardsHandler(); new Aside(); - gl.utils.initTimeagoTimeout(); + gl.utils.renderTimeago(); $(document).trigger('init.scrolling-tabs'); + + $('form.filter-form').on('submit', function (event) { + const link = document.createElement('a'); + link.href = this.action; + + const action = `${this.action}${link.search === '' ? '?' : '&'}`; + + event.preventDefault(); + gl.utils.visitUrl(`${action}${$(this).serialize()}`); + }); }); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index c4e379a4a0b..8be7314ded8 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -175,7 +175,7 @@ import Cookies from 'js-cookie'; getConflictsCountText() { const count = this.getConflictsCount(); - const text = count ? 'conflicts' : 'conflict'; + const text = count > 1 ? 'conflicts' : 'conflict'; return `${count} ${text}`; }, diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 17030c3e4d3..d74cf5328ad 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -2,6 +2,7 @@ /* global Flash */ import Vue from 'vue'; +import initIssuableSidebar from '../init_issuable_sidebar'; import './merge_conflict_store'; import './merge_conflict_service'; import './mixins/line_conflict_utils'; @@ -19,6 +20,8 @@ $(() => { resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath }); + initIssuableSidebar(); + gl.MergeConflictsResolverApp = new Vue({ el: '#conflicts', data: mergeConflictsStore.state, diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index f93feeec1c2..0db2abe507d 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -2,8 +2,9 @@ /* global MergeRequestTabs */ import 'vendor/jquery.waitforimages'; -import './task_list'; +import TaskList from './task_list'; import './merge_request_tabs'; +import IssuablesHelper from './helpers/issuables_helper'; (function() { this.MergeRequest = (function() { @@ -21,11 +22,14 @@ import './merge_request_tabs'; return _this.showAllCommits(); }; })(this)); + this.initTabs(); this.initMRBtnListeners(); this.initCommitMessageListeners(); + this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport(); + if ($("a.btn-close").length) { - this.taskList = new gl.TaskList({ + this.taskList = new TaskList({ dataType: 'merge_request', fieldName: 'description', selector: '.detail-page-description', @@ -64,11 +68,15 @@ import './merge_request_tabs'; if (shouldSubmit && $this.data('submitted')) { return; } + + if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable(); + if (shouldSubmit) { if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) { e.preventDefault(); e.stopImmediatePropagation(); - return _this.submitNoteForm($this.closest('form'), $this); + + _this.submitNoteForm($this.closest('form'), $this); } } }); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 7bb2236017e..7840f05a8ae 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -144,7 +144,9 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; this.resetViewContainer(); this.mountPipelinesView(); } else { - this.expandView(); + if (Breakpoints.get().getBreakpointSize() !== 'xs') { + this.expandView(); + } this.resetViewContainer(); this.destroyPipelinesView(); } @@ -155,7 +157,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; scrollToElement(container) { if (location.hash) { - const offset = -$('.js-tabs-affix').outerHeight(); + const offset = 0 - ( + $('.navbar-gitlab').outerHeight() + + $('.js-tabs-affix').outerHeight() + ); const $el = $(`${container} ${location.hash}:not(.match)`); if ($el.length) { $.scrollTo($el[0], { offset }); @@ -165,9 +170,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // Activate a tab based on the current action activateTab(action) { - const activate = action === 'show' ? 'notes' : action; // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(`.merge-request-tabs a[data-action='${activate}']`).tab('show'); + $(`.merge-request-tabs a[data-action='${action}']`).tab('show'); } // Replaces the current Merge Request-specific action in the URL with a new one @@ -182,7 +186,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // location.pathname # => "/namespace/project/merge_requests/1/diffs" // // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('notes') + // setCurrentAction('show') // location.pathname # => "/namespace/project/merge_requests/1" // // location.pathname # => "/namespace/project/merge_requests/1/diffs" @@ -191,13 +195,13 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // // Returns the new URL String setCurrentAction(action) { - this.currentAction = action === 'show' ? 'notes' : action; + this.currentAction = action; - // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs' - let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); + // Remove a trailing '/commits' '/diffs' '/pipelines' + let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, ''); // Append the new action if we're on a tab other than 'notes' - if (this.currentAction !== 'notes') { + if (this.currentAction !== 'show' && this.currentAction !== 'new') { newState += `/${this.currentAction}`; } @@ -291,7 +295,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; // Scroll any linked note into view // Similar to `toggler_behavior` in the discussion tab const hash = window.gl.utils.getLocationHash(); - const anchor = hash && $container.find(`[id="${hash}"]`); + const anchor = hash && $container.find(`.note[id="${hash}"]`); if (anchor && anchor.length > 0) { const notesContent = anchor.closest('.notes_content'); const lineType = notesContent.hasClass('new') ? 'new' : 'old'; @@ -301,6 +305,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; forceShow: true, }); anchor[0].scrollIntoView(); + window.gl.utils.handleLocationHash(); // We have multiple elements on the page with `#note_xxx` // (discussion and diff tabs) and `:target` only applies to the first anchor.addClass('target'); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 9d481d7c003..6756ab0b3aa 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -8,7 +8,7 @@ var _this, $els; if (currentProject != null) { _this = this; - this.currentProject = JSON.parse(currentProject); + this.currentProject = typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; } $els = $(els); diff --git a/app/assets/javascripts/monitoring/components/monitoring.vue b/app/assets/javascripts/monitoring/components/monitoring.vue new file mode 100644 index 00000000000..a6a2d3119e3 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring.vue @@ -0,0 +1,157 @@ +<script> + /* global Flash */ + import _ from 'underscore'; + import statusCodes from '../../lib/utils/http_status'; + import MonitoringService from '../services/monitoring_service'; + import monitoringRow from './monitoring_row.vue'; + import monitoringState from './monitoring_state.vue'; + import MonitoringStore from '../stores/monitoring_store'; + import eventHub from '../event_hub'; + + export default { + + data() { + const metricsData = document.querySelector('#prometheus-graphs').dataset; + const store = new MonitoringStore(); + + return { + store, + state: 'gettingStarted', + hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics), + documentationPath: metricsData.documentationPath, + settingsPath: metricsData.settingsPath, + endpoint: metricsData.additionalMetrics, + deploymentEndpoint: metricsData.deploymentEndpoint, + showEmptyState: true, + backOffRequestCounter: 0, + updateAspectRatio: false, + updatedAspectRatios: 0, + resizeThrottled: {}, + }; + }, + + components: { + monitoringRow, + monitoringState, + }, + + methods: { + getGraphsData() { + const maxNumberOfRequests = 3; + this.state = 'loading'; + gl.utils.backOff((next, stop) => { + this.service.get().then((resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + this.backOffRequestCounter = this.backOffRequestCounter += 1; + if (this.backOffRequestCounter < maxNumberOfRequests) { + next(); + } else { + stop(new Error('Failed to connect to the prometheus server')); + } + } else { + stop(resp); + } + }).catch(stop); + }) + .then((resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + this.state = 'unableToConnect'; + return false; + } + return resp.json(); + }) + .then((metricGroupsData) => { + if (!metricGroupsData) return false; + this.store.storeMetrics(metricGroupsData.data); + return this.getDeploymentData(); + }) + .then((deploymentData) => { + if (deploymentData !== false) { + this.store.storeDeploymentData(deploymentData.deployments); + this.showEmptyState = false; + } + return {}; + }) + .catch(() => { + this.state = 'unableToConnect'; + }); + }, + + getDeploymentData() { + return this.service.getDeploymentData(this.deploymentEndpoint) + .then(resp => resp.json()) + .catch(() => new Flash('Error getting deployment information.')); + }, + + resize() { + this.updateAspectRatio = true; + }, + + toggleAspectRatio() { + this.updatedAspectRatios = this.updatedAspectRatios += 1; + if (this.store.getMetricsCount() === this.updatedAspectRatios) { + this.updateAspectRatio = !this.updateAspectRatio; + this.updatedAspectRatios = 0; + } + }, + + }, + + created() { + this.service = new MonitoringService(this.endpoint); + eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); + }, + + beforeDestroy() { + eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); + window.removeEventListener('resize', this.resizeThrottled, false); + }, + + mounted() { + this.resizeThrottled = _.throttle(this.resize, 600); + if (!this.hasMetrics) { + this.state = 'gettingStarted'; + } else { + this.getGraphsData(); + window.addEventListener('resize', this.resizeThrottled, false); + } + }, + }; +</script> +<template> + <div + class="prometheus-graphs" + v-if="!showEmptyState"> + <div + class="row" + v-for="(groupData, index) in store.groups" + :key="index"> + <div + class="col-md-12"> + <div + class="panel panel-default prometheus-panel"> + <div + class="panel-heading"> + <h4>{{groupData.group}}</h4> + </div> + <div + class="panel-body"> + <monitoring-row + v-for="(row, index) in groupData.metrics" + :key="index" + :row-data="row" + :update-aspect-ratio="updateAspectRatio" + :deployment-data="store.deploymentData" + /> + </div> + </div> + </div> + </div> + </div> + <monitoring-state + :selected-state="state" + :documentation-path="documentationPath" + :settings-path="settingsPath" + v-else + /> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_column.vue b/app/assets/javascripts/monitoring/components/monitoring_column.vue new file mode 100644 index 00000000000..c376baea79c --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_column.vue @@ -0,0 +1,297 @@ +<script> + /* global Breakpoints */ + import d3 from 'd3'; + import monitoringLegends from './monitoring_legends.vue'; + import monitoringFlag from './monitoring_flag.vue'; + import monitoringDeployment from './monitoring_deployment.vue'; + import MonitoringMixin from '../mixins/monitoring_mixins'; + import eventHub from '../event_hub'; + import measurements from '../utils/measurements'; + import { formatRelevantDigits } from '../../lib/utils/number_utils'; + + const bisectDate = d3.bisector(d => d.time).left; + + export default { + props: { + columnData: { + type: Object, + required: true, + }, + classType: { + type: String, + required: true, + }, + updateAspectRatio: { + type: Boolean, + required: true, + }, + deploymentData: { + type: Array, + required: true, + }, + }, + + mixins: [MonitoringMixin], + + data() { + return { + graphHeight: 450, + graphWidth: 600, + graphHeightOffset: 120, + xScale: {}, + yScale: {}, + margin: {}, + data: [], + breakpointHandler: Breakpoints.get(), + unitOfDisplay: '', + areaColorRgb: '#8fbce8', + lineColorRgb: '#1f78d1', + yAxisLabel: '', + legendTitle: '', + reducedDeploymentData: [], + area: '', + line: '', + measurements: measurements.large, + currentData: { + time: new Date(), + value: 0, + }, + currentYCoordinate: 0, + currentXCoordinate: 0, + currentFlagPosition: 0, + metricUsage: '', + showFlag: false, + showDeployInfo: true, + }; + }, + + components: { + monitoringLegends, + monitoringFlag, + monitoringDeployment, + }, + + computed: { + outterViewBox() { + return `0 0 ${this.graphWidth} ${this.graphHeight}`; + }, + + innerViewBox() { + if ((this.graphWidth - 150) > 0) { + return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`; + } + return '0 0 0 0'; + }, + + axisTransform() { + return `translate(70, ${this.graphHeight - 100})`; + }, + + paddingBottomRootSvg() { + return { + paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`, + }; + }, + }, + + methods: { + draw() { + const breakpointSize = this.breakpointHandler.getBreakpointSize(); + const query = this.columnData.queries[0]; + this.margin = measurements.large.margin; + if (breakpointSize === 'xs' || breakpointSize === 'sm') { + this.graphHeight = 300; + this.margin = measurements.small.margin; + this.measurements = measurements.small; + } + this.data = query.result[0].values; + this.unitOfDisplay = query.unit || ''; + this.yAxisLabel = this.columnData.y_label || 'Values'; + this.legendTitle = query.label || 'Average'; + this.graphWidth = this.$refs.baseSvg.clientWidth - + this.margin.left - this.margin.right; + this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; + if (this.data !== undefined) { + this.renderAxesPaths(); + this.formatDeployments(); + } + }, + + handleMouseOverGraph(e) { + let point = this.$refs.graphData.createSVGPoint(); + point.x = e.clientX; + point.y = e.clientY; + point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); + point.x = point.x += 7; + const timeValueOverlay = this.xScale.invert(point.x); + const overlayIndex = bisectDate(this.data, timeValueOverlay, 1); + const d0 = this.data[overlayIndex - 1]; + const d1 = this.data[overlayIndex]; + if (d0 === undefined || d1 === undefined) return; + const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; + this.currentData = evalTime ? d1 : d0; + this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time)); + const currentDeployXPos = this.mouseOverDeployInfo(point.x); + this.currentYCoordinate = this.yScale(this.currentData.value); + + if (this.currentXCoordinate > (this.graphWidth - 200)) { + this.currentFlagPosition = this.currentXCoordinate - 103; + } else { + this.currentFlagPosition = this.currentXCoordinate; + } + + if (currentDeployXPos) { + this.showFlag = false; + } else { + this.showFlag = true; + } + + this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`; + }, + + renderAxesPaths() { + const axisXScale = d3.time.scale() + .range([0, this.graphWidth]); + this.yScale = d3.scale.linear() + .range([this.graphHeight - this.graphHeightOffset, 0]); + axisXScale.domain(d3.extent(this.data, d => d.time)); + this.yScale.domain([0, d3.max(this.data.map(d => d.value))]); + + const xAxis = d3.svg.axis() + .scale(axisXScale) + .ticks(measurements.xTicks) + .orient('bottom'); + + const yAxis = d3.svg.axis() + .scale(this.yScale) + .ticks(measurements.yTicks) + .orient('left'); + + d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis); + + const width = this.graphWidth; + d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis) + .selectAll('.tick') + .each(function createTickLines(d, i) { + if (i > 0) { + d3.select(this).select('line') + .attr('x2', width) + .attr('class', 'axis-tick'); + } // Avoid adding the class to the first tick, to prevent coloring + }); // This will select all of the ticks once they're rendered + + this.xScale = d3.time.scale() + .range([0, this.graphWidth - 70]); + + this.xScale.domain(d3.extent(this.data, d => d.time)); + + const areaFunction = d3.svg.area() + .x(d => this.xScale(d.time)) + .y0(this.graphHeight - this.graphHeightOffset) + .y1(d => this.yScale(d.value)) + .interpolate('linear'); + + const lineFunction = d3.svg.line() + .x(d => this.xScale(d.time)) + .y(d => this.yScale(d.value)); + + this.line = lineFunction(this.data); + + this.area = areaFunction(this.data); + }, + }, + + watch: { + updateAspectRatio() { + if (this.updateAspectRatio) { + this.graphHeight = 450; + this.graphWidth = 600; + this.measurements = measurements.large; + this.draw(); + eventHub.$emit('toggleAspectRatio'); + } + }, + }, + + mounted() { + this.draw(); + }, + }; +</script> +<template> + <div + :class="classType"> + <h5 + class="text-center graph-title"> + {{columnData.title}} + </h5> + <div + class="prometheus-svg-container" + :style="paddingBottomRootSvg"> + <svg + :viewBox="outterViewBox" + ref="baseSvg"> + <g + class="x-axis" + :transform="axisTransform"> + </g> + <g + class="y-axis" + transform="translate(70, 20)"> + </g> + <monitoring-legends + :graph-width="graphWidth" + :graph-height="graphHeight" + :margin="margin" + :measurements="measurements" + :area-color-rgb="areaColorRgb" + :legend-title="legendTitle" + :y-axis-label="yAxisLabel" + :metric-usage="metricUsage" + /> + <svg + class="graph-data" + :viewBox="innerViewBox" + ref="graphData"> + <path + class="metric-area" + :d="area" + :fill="areaColorRgb" + transform="translate(-5, 20)"> + </path> + <path + class="metric-line" + :d="line" + :stroke="lineColorRgb" + fill="none" + stroke-width="2" + transform="translate(-5, 20)"> + </path> + <rect + class="prometheus-graph-overlay" + :width="(graphWidth - 70)" + :height="(graphHeight - 100)" + transform="translate(-5, 20)" + ref="graphOverlay" + @mousemove="handleMouseOverGraph($event)"> + </rect> + <monitoring-deployment + :show-deploy-info="showDeployInfo" + :deployment-data="reducedDeploymentData" + :graph-height="graphHeight" + :graph-height-offset="graphHeightOffset" + /> + <monitoring-flag + v-if="showFlag" + :current-x-coordinate="currentXCoordinate" + :current-y-coordinate="currentYCoordinate" + :current-data="currentData" + :current-flag-position="currentFlagPosition" + :graph-height="graphHeight" + :graph-height-offset="graphHeightOffset" + /> + </svg> + </svg> + </div> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_deployment.vue b/app/assets/javascripts/monitoring/components/monitoring_deployment.vue new file mode 100644 index 00000000000..e6432ba3191 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_deployment.vue @@ -0,0 +1,136 @@ +<script> + import { + dateFormat, + timeFormat, + } from '../constants'; + + export default { + props: { + showDeployInfo: { + type: Boolean, + required: true, + }, + deploymentData: { + type: Array, + required: true, + }, + graphHeight: { + type: Number, + required: true, + }, + graphHeightOffset: { + type: Number, + required: true, + }, + }, + + computed: { + calculatedHeight() { + return this.graphHeight - this.graphHeightOffset; + }, + }, + + methods: { + refText(d) { + return d.tag ? d.ref : d.sha.slice(0, 6); + }, + + formatTime(deploymentTime) { + return timeFormat(deploymentTime); + }, + + formatDate(deploymentTime) { + return dateFormat(deploymentTime); + }, + + nameDeploymentClass(deployment) { + return `deploy-info-${deployment.id}`; + }, + + transformDeploymentGroup(deployment) { + return `translate(${Math.floor(deployment.xPos) + 1}, 20)`; + }, + }, + }; +</script> +<template> + <g + class="deploy-info" + v-if="showDeployInfo"> + <g + v-for="(deployment, index) in deploymentData" + :key="index" + :class="nameDeploymentClass(deployment)" + :transform="transformDeploymentGroup(deployment)"> + <rect + x="0" + y="0" + :height="calculatedHeight" + width="3" + fill="url(#shadow-gradient)"> + </rect> + <line + class="deployment-line" + x1="0" + y1="0" + x2="0" + :y2="calculatedHeight" + stroke="#000"> + </line> + <svg + v-if="deployment.showDeploymentFlag" + class="js-deploy-info-box" + x="3" + y="0" + width="92" + height="60"> + <rect + class="rect-text-metric deploy-info-rect rect-metric" + x="1" + y="1" + rx="2" + width="90" + height="58"> + </rect> + <g + transform="translate(5, 2)"> + <text + class="deploy-info-text text-metric-bold"> + {{refText(deployment)}} + </text> + </g> + <text + class="deploy-info-text" + y="18" + transform="translate(5, 2)"> + {{formatDate(deployment.time)}} + </text> + <text + class="deploy-info-text text-metric-bold" + y="38" + transform="translate(5, 2)"> + {{formatTime(deployment.time)}} + </text> + </svg> + </g> + <svg + height="0" + width="0"> + <defs> + <linearGradient + id="shadow-gradient"> + <stop + offset="0%" + stop-color="#000" + stop-opacity="0.4"> + </stop> + <stop + offset="100%" + stop-color="#000" + stop-opacity="0"> + </stop> + </linearGradient> + </defs> + </svg> + </g> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_flag.vue b/app/assets/javascripts/monitoring/components/monitoring_flag.vue new file mode 100644 index 00000000000..5a0e50fcab3 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_flag.vue @@ -0,0 +1,104 @@ +<script> + import { + dateFormat, + timeFormat, + } from '../constants'; + + export default { + props: { + currentXCoordinate: { + type: Number, + required: true, + }, + currentYCoordinate: { + type: Number, + required: true, + }, + currentFlagPosition: { + type: Number, + required: true, + }, + currentData: { + type: Object, + required: true, + }, + graphHeight: { + type: Number, + required: true, + }, + graphHeightOffset: { + type: Number, + required: true, + }, + }, + + data() { + return { + circleColorRgb: '#8fbce8', + }; + }, + + computed: { + formatTime() { + return timeFormat(this.currentData.time); + }, + + formatDate() { + return dateFormat(this.currentData.time); + }, + + calculatedHeight() { + return this.graphHeight - this.graphHeightOffset; + }, + }, + }; +</script> +<template> + <g class="mouse-over-flag"> + <line + class="selected-metric-line" + :x1="currentXCoordinate" + :y1="0" + :x2="currentXCoordinate" + :y2="calculatedHeight" + transform="translate(-5, 20)"> + </line> + <circle + class="circle-metric" + :fill="circleColorRgb" + stroke="#000" + :cx="currentXCoordinate" + :cy="currentYCoordinate" + r="5" + transform="translate(-5, 20)"> + </circle> + <svg + class="rect-text-metric" + :x="currentFlagPosition" + y="0"> + <rect + class="rect-metric" + x="4" + y="1" + rx="2" + width="90" + height="40" + transform="translate(-3, 20)"> + </rect> + <text + class="text-metric text-metric-bold" + x="16" + y="35" + transform="translate(-5, 20)"> + {{formatTime}} + </text> + <text + class="text-metric" + x="16" + y="15" + transform="translate(-5, 20)"> + {{formatDate}} + </text> + </svg> + </g> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_legends.vue b/app/assets/javascripts/monitoring/components/monitoring_legends.vue new file mode 100644 index 00000000000..922a5e1bf0e --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_legends.vue @@ -0,0 +1,144 @@ +<script> + export default { + props: { + graphWidth: { + type: Number, + required: true, + }, + graphHeight: { + type: Number, + required: true, + }, + margin: { + type: Object, + required: true, + }, + measurements: { + type: Object, + required: true, + }, + areaColorRgb: { + type: String, + required: true, + }, + legendTitle: { + type: String, + required: true, + }, + yAxisLabel: { + type: String, + required: true, + }, + metricUsage: { + type: String, + required: true, + }, + }, + data() { + return { + yLabelWidth: 0, + yLabelHeight: 0, + }; + }, + computed: { + textTransform() { + const yCoordinate = (((this.graphHeight - this.margin.top) + + this.measurements.axisLabelLineOffset) / 2) || 0; + + return `translate(15, ${yCoordinate}) rotate(-90)`; + }, + + rectTransform() { + const yCoordinate = ((this.graphHeight - this.margin.top) / 2) + + (this.yLabelWidth / 2) + 10 || 0; + + return `translate(0, ${yCoordinate}) rotate(-90)`; + }, + + xPosition() { + return (((this.graphWidth + this.measurements.axisLabelLineOffset) / 2) + - this.margin.right) || 0; + }, + + yPosition() { + return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0; + }, + }, + mounted() { + this.$nextTick(() => { + const bbox = this.$refs.ylabel.getBBox(); + this.yLabelWidth = bbox.width + 10; // Added some padding + this.yLabelHeight = bbox.height + 5; + }); + }, + }; +</script> +<template> + <g + class="axis-label-container"> + <line + class="label-x-axis-line" + stroke="#000000" + stroke-width="1" + x1="10" + :y1="yPosition" + :x2="graphWidth + 20" + :y2="yPosition"> + </line> + <line + class="label-y-axis-line" + stroke="#000000" + stroke-width="1" + x1="10" + y1="0" + :x2="10" + :y2="yPosition"> + </line> + <rect + class="rect-axis-text" + :transform="rectTransform" + :width="yLabelWidth" + :height="yLabelHeight"> + </rect> + <text + class="label-axis-text y-label-text" + text-anchor="middle" + :transform="textTransform" + ref="ylabel"> + {{yAxisLabel}} + </text> + <rect + class="rect-axis-text" + :x="xPosition + 60" + :y="graphHeight - 80" + width="35" + height="50"> + </rect> + <text + class="label-axis-text x-label-text" + :x="xPosition + 60" + :y="yPosition" + dy=".35em"> + Time + </text> + <rect + :fill="areaColorRgb" + :width="measurements.legends.width" + :height="measurements.legends.height" + x="20" + :y="graphHeight - measurements.legendOffset"> + </rect> + <text + class="text-metric-title" + x="50" + :y="graphHeight - 25"> + {{legendTitle}} + </text> + <text + class="text-metric-usage" + x="50" + :y="graphHeight - 10"> + {{metricUsage}} + </text> + </g> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_row.vue b/app/assets/javascripts/monitoring/components/monitoring_row.vue new file mode 100644 index 00000000000..e5528f17880 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_row.vue @@ -0,0 +1,41 @@ +<script> + import monitoringColumn from './monitoring_column.vue'; + + export default { + props: { + rowData: { + type: Array, + required: true, + }, + updateAspectRatio: { + type: Boolean, + required: true, + }, + deploymentData: { + type: Array, + required: true, + }, + }, + components: { + monitoringColumn, + }, + computed: { + bootstrapClass() { + return this.rowData.length >= 2 ? 'col-md-6' : 'col-md-12'; + }, + }, + }; +</script> +<template> + <div + class="prometheus-row row"> + <monitoring-column + v-for="(column, index) in rowData" + :column-data="column" + :class-type="bootstrapClass" + :key="index" + :update-aspect-ratio="updateAspectRatio" + :deployment-data="deploymentData" + /> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/monitoring_state.vue b/app/assets/javascripts/monitoring/components/monitoring_state.vue new file mode 100644 index 00000000000..598021aa4df --- /dev/null +++ b/app/assets/javascripts/monitoring/components/monitoring_state.vue @@ -0,0 +1,112 @@ +<script> + import gettingStartedSvg from 'empty_states/monitoring/_getting_started.svg'; + import loadingSvg from 'empty_states/monitoring/_loading.svg'; + import unableToConnectSvg from 'empty_states/monitoring/_unable_to_connect.svg'; + + export default { + props: { + documentationPath: { + type: String, + required: true, + }, + settingsPath: { + type: String, + required: false, + default: '', + }, + selectedState: { + type: String, + required: true, + }, + }, + data() { + return { + states: { + gettingStarted: { + svg: gettingStartedSvg, + title: 'Get started with performance monitoring', + description: 'Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.', + buttonText: 'Configure Prometheus', + }, + loading: { + svg: loadingSvg, + title: 'Waiting for performance data', + description: 'Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.', + buttonText: 'View documentation', + }, + unableToConnect: { + svg: unableToConnectSvg, + title: 'Unable to connect to Prometheus server', + description: 'Ensure connectivity is available from the GitLab server to the ', + buttonText: 'View documentation', + }, + }, + }; + }, + computed: { + currentState() { + return this.states[this.selectedState]; + }, + + buttonPath() { + if (this.selectedState === 'gettingStarted') { + return this.settingsPath; + } + return this.documentationPath; + }, + + showButtonDescription() { + if (this.selectedState === 'unableToConnect') return true; + return false; + }, + }, + }; +</script> +<template> + <div + class="prometheus-state"> + <div + class="row"> + <div + class="col-md-4 col-md-offset-4 state-svg" + v-html="currentState.svg"> + </div> + </div> + <div + class="row"> + <div + class="col-md-6 col-md-offset-3"> + <h4 + class="text-center state-title"> + {{currentState.title}} + </h4> + </div> + </div> + <div + class="row"> + <div + class="col-md-6 col-md-offset-3"> + <div + class="description-text text-center state-description"> + {{currentState.description}} + <a + :href="settingsPath" + v-if="showButtonDescription"> + Prometheus server + </a> + </div> + </div> + </div> + <div + class="row state-button-section"> + <div + class="col-md-4 col-md-offset-4 text-center state-button"> + <a + class="btn btn-success" + :href="buttonPath"> + {{currentState.buttonText}} + </a> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/deployments.js b/app/assets/javascripts/monitoring/deployments.js deleted file mode 100644 index fc92ab61b31..00000000000 --- a/app/assets/javascripts/monitoring/deployments.js +++ /dev/null @@ -1,211 +0,0 @@ -/* global Flash */ -import d3 from 'd3'; -import { - dateFormat, - timeFormat, -} from './constants'; - -export default class Deployments { - constructor(width, height) { - this.width = width; - this.height = height; - - this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint; - - this.createGradientDef(); - } - - init(chartData) { - this.chartData = chartData; - - this.x = d3.time.scale().range([0, this.width]); - this.x.domain(d3.extent(this.chartData, d => d.time)); - - this.charts = d3.selectAll('.prometheus-graph'); - - this.getData(); - } - - getData() { - $.ajax({ - url: this.endpoint, - dataType: 'JSON', - }) - .fail(() => new Flash('Error getting deployment information.')) - .done((data) => { - this.data = data.deployments.reduce((deploymentDataArray, deployment) => { - const time = new Date(deployment.created_at); - const xPos = Math.floor(this.x(time)); - - time.setSeconds(this.chartData[0].time.getSeconds()); - - if (xPos >= 0) { - deploymentDataArray.push({ - id: deployment.id, - time, - sha: deployment.sha, - tag: deployment.tag, - ref: deployment.ref.name, - xPos, - }); - } - - return deploymentDataArray; - }, []); - - this.plotData(); - }); - } - - plotData() { - this.charts.each((d, i) => { - const svg = d3.select(this.charts[0][i]); - const chart = svg.select('.graph-container'); - const key = svg.node().getAttribute('graph-type'); - - this.createLine(chart, key); - this.createDeployInfoBox(chart, key); - }); - } - - createGradientDef() { - const defs = d3.select('body') - .append('svg') - .attr({ - height: 0, - width: 0, - }) - .append('defs'); - - defs.append('linearGradient') - .attr({ - id: 'shadow-gradient', - }) - .append('stop') - .attr({ - offset: '0%', - 'stop-color': '#000', - 'stop-opacity': 0.4, - }) - .select(this.selectParentNode) - .append('stop') - .attr({ - offset: '100%', - 'stop-color': '#000', - 'stop-opacity': 0, - }); - } - - createLine(chart, key) { - chart.append('g') - .attr({ - class: 'deploy-info', - }) - .selectAll('.deploy-info') - .data(this.data) - .enter() - .append('g') - .attr({ - class: d => `deploy-info-${d.id}-${key}`, - transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`, - }) - .append('rect') - .attr({ - x: 1, - y: 0, - height: this.height + 1, - width: 3, - fill: 'url(#shadow-gradient)', - }) - .select(this.selectParentNode) - .append('line') - .attr({ - class: 'deployment-line', - x1: 0, - x2: 0, - y1: 0, - y2: this.height + 1, - }); - } - - createDeployInfoBox(chart, key) { - chart.selectAll('.deploy-info') - .selectAll('.js-deploy-info-box') - .data(this.data) - .enter() - .select(d => document.querySelector(`.deploy-info-${d.id}-${key}`)) - .append('svg') - .attr({ - class: 'js-deploy-info-box hidden', - x: 3, - y: 0, - width: 92, - height: 60, - }) - .append('rect') - .attr({ - class: 'rect-text-metric deploy-info-rect rect-metric', - x: 1, - y: 1, - rx: 2, - width: 90, - height: 58, - }) - .select(this.selectParentNode) - .append('g') - .attr({ - transform: 'translate(5, 2)', - }) - .append('text') - .attr({ - class: 'deploy-info-text text-metric-bold', - }) - .text(Deployments.refText) - .select(this.selectParentNode) - .append('text') - .attr({ - class: 'deploy-info-text', - y: 18, - }) - .text(d => dateFormat(d.time)) - .select(this.selectParentNode) - .append('text') - .attr({ - class: 'deploy-info-text text-metric-bold', - y: 38, - }) - .text(d => timeFormat(d.time)); - } - - static toggleDeployTextbox(deploy, key, showInfoBox) { - d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`) - .classed('hidden', !showInfoBox); - } - - mouseOverDeployInfo(mouseXPos, key) { - if (!this.data) return false; - - let dataFound = false; - - this.data.forEach((d) => { - if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) { - dataFound = d.xPos + 1; - - Deployments.toggleDeployTextbox(d, key, true); - } else { - Deployments.toggleDeployTextbox(d, key, false); - } - }); - - return dataFound; - } - - /* `this` is bound to the D3 node */ - selectParentNode() { - return this.parentNode; - } - - static refText(d) { - return d.tag ? d.ref : d.sha.slice(0, 6); - } -} diff --git a/app/assets/javascripts/monitoring/event_hub.js b/app/assets/javascripts/monitoring/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/monitoring/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js new file mode 100644 index 00000000000..8e62fa63f13 --- /dev/null +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -0,0 +1,46 @@ +const mixins = { + methods: { + mouseOverDeployInfo(mouseXPos) { + if (!this.reducedDeploymentData) return false; + + let dataFound = false; + this.reducedDeploymentData = this.reducedDeploymentData.map((d) => { + const deployment = d; + if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) { + dataFound = d.xPos + 1; + + deployment.showDeploymentFlag = true; + } else { + deployment.showDeploymentFlag = false; + } + return deployment; + }); + + return dataFound; + }, + formatDeployments() { + this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { + const time = new Date(deployment.created_at); + const xPos = Math.floor(this.xScale(time)); + + time.setSeconds(this.data[0].time.getSeconds()); + + if (xPos >= 0) { + deploymentDataArray.push({ + id: deployment.id, + time, + sha: deployment.sha, + tag: deployment.tag, + ref: deployment.ref.name, + xPos, + showDeploymentFlag: false, + }); + } + + return deploymentDataArray; + }, []); + }, + }, +}; + +export default mixins; diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index b3ce9310417..5d5cb56af72 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,6 +1,10 @@ -import PrometheusGraph from './prometheus_graph'; +import Vue from 'vue'; +import Monitoring from './components/monitoring.vue'; -document.addEventListener('DOMContentLoaded', function onLoad() { - document.removeEventListener('DOMContentLoaded', onLoad, false); - return new PrometheusGraph(); -}, false); +document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#prometheus-graphs', + components: { + 'monitoring-dashboard': Monitoring, + }, + render: createElement => createElement('monitoring-dashboard'), +})); diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js deleted file mode 100644 index 6af88769129..00000000000 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ /dev/null @@ -1,433 +0,0 @@ -/* eslint-disable no-new */ -/* global Flash */ - -import d3 from 'd3'; -import statusCodes from '~/lib/utils/http_status'; -import Deployments from './deployments'; -import '../lib/utils/common_utils'; -import { formatRelevantDigits } from '../lib/utils/number_utils'; -import '../flash'; -import { - dateFormat, - timeFormat, -} from './constants'; - -const prometheusContainer = '.prometheus-container'; -const prometheusParentGraphContainer = '.prometheus-graphs'; -const prometheusGraphsContainer = '.prometheus-graph'; -const prometheusStatesContainer = '.prometheus-state'; -const metricsEndpoint = 'metrics.json'; -const bisectDate = d3.bisector(d => d.time).left; -const extraAddedWidthParent = 100; - -class PrometheusGraph { - constructor() { - const $prometheusContainer = $(prometheusContainer); - const hasMetrics = $prometheusContainer.data('has-metrics'); - this.docLink = $prometheusContainer.data('doc-link'); - this.integrationLink = $prometheusContainer.data('prometheus-integration'); - this.state = ''; - - $(document).ajaxError(() => {}); - - if (hasMetrics) { - this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; - this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; - const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + - extraAddedWidthParent; - this.originalWidth = parentContainerWidth; - this.originalHeight = 330; - this.width = parentContainerWidth - this.margin.left - this.margin.right; - this.height = this.originalHeight - this.margin.top - this.margin.bottom; - this.backOffRequestCounter = 0; - this.deployments = new Deployments(this.width, this.height); - this.configureGraph(); - this.init(); - } else { - const prevState = this.state; - this.state = '.js-getting-started'; - this.updateState(prevState); - } - } - - createGraph() { - Object.keys(this.graphSpecificProperties).forEach((key) => { - const value = this.graphSpecificProperties[key]; - if (value.data.length > 0) { - this.plotValues(key); - } - }); - } - - init() { - return this.getData().then((metricsResponse) => { - let enoughData = true; - if (typeof metricsResponse === 'undefined') { - enoughData = false; - } else { - Object.keys(metricsResponse.metrics).forEach((key) => { - if (key === 'cpu_values' || key === 'memory_values') { - const currentData = (metricsResponse.metrics[key])[0]; - if (currentData.values.length <= 2) { - enoughData = false; - } - } - }); - } - if (enoughData) { - $(prometheusStatesContainer).hide(); - $(prometheusParentGraphContainer).show(); - this.transformData(metricsResponse); - this.createGraph(); - - const firstMetricData = this.graphSpecificProperties[ - Object.keys(this.graphSpecificProperties)[0] - ].data; - - this.deployments.init(firstMetricData); - } - }); - } - - plotValues(key) { - const graphSpecifics = this.graphSpecificProperties[key]; - - const x = d3.time.scale() - .range([0, this.width]); - - const y = d3.scale.linear() - .range([this.height, 0]); - - graphSpecifics.xScale = x; - graphSpecifics.yScale = y; - - const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; - - const chart = d3.select(prometheusGraphContainer) - .attr('width', this.width + this.margin.left + this.margin.right) - .attr('height', this.height + this.margin.bottom + this.margin.top) - .append('g') - .attr('class', 'graph-container') - .attr('transform', `translate(${this.margin.left},${this.margin.top})`); - - const axisLabelContainer = d3.select(prometheusGraphContainer) - .attr('width', this.originalWidth) - .attr('height', this.originalHeight) - .append('g') - .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); - - x.domain(d3.extent(graphSpecifics.data, d => d.time)); - y.domain([0, d3.max(graphSpecifics.data.map(metricValue => metricValue.value))]); - - const xAxis = d3.svg.axis() - .scale(x) - .ticks(this.commonGraphProperties.axis_no_ticks) - .orient('bottom'); - - const yAxis = d3.svg.axis() - .scale(y) - .ticks(this.commonGraphProperties.axis_no_ticks) - .tickSize(-this.width) - .outerTickSize(0) - .orient('left'); - - this.createAxisLabelContainers(axisLabelContainer, key); - - chart.append('g') - .attr('class', 'x-axis') - .attr('transform', `translate(0,${this.height})`) - .call(xAxis); - - chart.append('g') - .attr('class', 'y-axis') - .call(yAxis); - - const area = d3.svg.area() - .x(d => x(d.time)) - .y0(this.height) - .y1(d => y(d.value)) - .interpolate('linear'); - - const line = d3.svg.line() - .x(d => x(d.time)) - .y(d => y(d.value)); - - chart.append('path') - .datum(graphSpecifics.data) - .attr('d', area) - .attr('class', 'metric-area') - .attr('fill', graphSpecifics.area_fill_color); - - chart.append('path') - .datum(graphSpecifics.data) - .attr('class', 'metric-line') - .attr('stroke', graphSpecifics.line_color) - .attr('fill', 'none') - .attr('stroke-width', this.commonGraphProperties.area_stroke_width) - .attr('d', line); - - // Overlay area for the mouseover events - chart.append('rect') - .attr('class', 'prometheus-graph-overlay') - .attr('width', this.width) - .attr('height', this.height) - .on('mousemove', this.handleMouseOverGraph.bind(this, prometheusGraphContainer)); - } - - // The legends from the metric - createAxisLabelContainers(axisLabelContainer, key) { - const graphSpecifics = this.graphSpecificProperties[key]; - - axisLabelContainer.append('line') - .attr('class', 'label-x-axis-line') - .attr('stroke', '#000000') - .attr('stroke-width', '1') - .attr({ - x1: 10, - y1: this.originalHeight - this.margin.top, - x2: (this.originalWidth - this.margin.right) + 10, - y2: this.originalHeight - this.margin.top, - }); - - axisLabelContainer.append('line') - .attr('class', 'label-y-axis-line') - .attr('stroke', '#000000') - .attr('stroke-width', '1') - .attr({ - x1: 10, - y1: 0, - x2: 10, - y2: this.originalHeight - this.margin.top, - }); - - axisLabelContainer.append('rect') - .attr('class', 'rect-axis-text') - .attr('x', 0) - .attr('y', 50) - .attr('width', 30) - .attr('height', 150); - - axisLabelContainer.append('text') - .attr('class', 'label-axis-text') - .attr('text-anchor', 'middle') - .attr('transform', `translate(15, ${(this.originalHeight - this.margin.top) / 2}) rotate(-90)`) - .text(graphSpecifics.graph_legend_title); - - axisLabelContainer.append('rect') - .attr('class', 'rect-axis-text') - .attr('x', (this.originalWidth / 2) - this.margin.right) - .attr('y', this.originalHeight - 100) - .attr('width', 30) - .attr('height', 80); - - axisLabelContainer.append('text') - .attr('class', 'label-axis-text') - .attr('x', (this.originalWidth / 2) - this.margin.right) - .attr('y', this.originalHeight - this.margin.top) - .attr('dy', '.35em') - .text('Time'); - - // Legends - - // Metric Usage - axisLabelContainer.append('rect') - .attr('x', this.originalWidth - 170) - .attr('y', (this.originalHeight / 2) - 60) - .style('fill', graphSpecifics.area_fill_color) - .attr('width', 20) - .attr('height', 35); - - axisLabelContainer.append('text') - .attr('class', 'text-metric-title') - .attr('x', this.originalWidth - 140) - .attr('y', (this.originalHeight / 2) - 50) - .text('Average'); - - axisLabelContainer.append('text') - .attr('class', 'text-metric-usage') - .attr('x', this.originalWidth - 140) - .attr('y', (this.originalHeight / 2) - 25); - } - - handleMouseOverGraph(prometheusGraphContainer) { - const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`); - const currentXCoordinate = d3.mouse(rectOverlay)[0]; - - Object.keys(this.graphSpecificProperties).forEach((key) => { - const currentGraphProps = this.graphSpecificProperties[key]; - const timeValueOverlay = currentGraphProps.xScale.invert(currentXCoordinate); - const overlayIndex = bisectDate(currentGraphProps.data, timeValueOverlay, 1); - const d0 = currentGraphProps.data[overlayIndex - 1]; - const d1 = currentGraphProps.data[overlayIndex]; - const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay; - const currentData = evalTime ? d1 : d0; - const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time)); - const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key); - const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; - const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value)); - const maxMetricValue = currentGraphProps.yScale(maxValueFromData); - - // Clear up all the pieces of the flag - d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove(); - d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove(); - d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove(); - - const currentChart = d3.select(currentPrometheusGraphContainer).select('g'); - currentChart.append('line') - .attr({ - class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`, - x1: currentTimeCoordinate, - y1: currentGraphProps.yScale(0), - x2: currentTimeCoordinate, - y2: maxMetricValue, - }); - - currentChart.append('circle') - .attr('class', 'circle-metric') - .attr('fill', currentGraphProps.line_color) - .attr('cx', currentDeployXPos || currentTimeCoordinate) - .attr('cy', currentGraphProps.yScale(currentData.value)) - .attr('r', this.commonGraphProperties.circle_radius_metric); - - if (currentDeployXPos) return; - - // The little box with text - const rectTextMetric = currentChart.append('svg') - .attr({ - class: 'rect-text-metric', - x: currentTimeCoordinate, - y: 0, - }); - - rectTextMetric.append('rect') - .attr({ - class: 'rect-metric', - x: 4, - y: 1, - rx: 2, - width: this.commonGraphProperties.rect_text_width, - height: this.commonGraphProperties.rect_text_height, - }); - - rectTextMetric.append('text') - .attr({ - class: 'text-metric text-metric-bold', - x: 8, - y: 35, - }) - .text(timeFormat(currentData.time)); - - rectTextMetric.append('text') - .attr({ - class: 'text-metric-date', - x: 8, - y: 15, - }) - .text(dateFormat(currentData.time)); - - let currentMetricValue = formatRelevantDigits(currentData.value); - if (key === 'cpu_values') { - currentMetricValue = `${currentMetricValue}%`; - } else { - currentMetricValue = `${currentMetricValue} MB`; - } - - d3.select(`${currentPrometheusGraphContainer} .text-metric-usage`) - .text(currentMetricValue); - }); - } - - configureGraph() { - this.graphSpecificProperties = { - cpu_values: { - area_fill_color: '#edf3fc', - line_color: '#5b99f7', - graph_legend_title: 'CPU Usage (Cores)', - data: [], - xScale: {}, - yScale: {}, - }, - memory_values: { - area_fill_color: '#fca326', - line_color: '#fc6d26', - graph_legend_title: 'Memory Usage (MB)', - data: [], - xScale: {}, - yScale: {}, - }, - }; - - this.commonGraphProperties = { - area_stroke_width: 2, - median_total_characters: 8, - circle_radius_metric: 5, - rect_text_width: 90, - rect_text_height: 40, - axis_no_ticks: 3, - }; - } - - getData() { - const maxNumberOfRequests = 3; - this.state = '.js-loading'; - this.updateState(); - return gl.utils.backOff((next, stop) => { - $.ajax({ - url: metricsEndpoint, - dataType: 'json', - }) - .done((data, statusText, resp) => { - if (resp.status === statusCodes.NO_CONTENT) { - this.backOffRequestCounter = this.backOffRequestCounter += 1; - if (this.backOffRequestCounter < maxNumberOfRequests) { - next(); - } else if (this.backOffRequestCounter >= maxNumberOfRequests) { - stop(new Error('loading')); - } - } else if (!data.success) { - stop(new Error('loading')); - } else { - stop({ - status: resp.status, - metrics: data, - }); - } - }).fail(stop); - }) - .then((resp) => { - if (resp.status === statusCodes.NO_CONTENT) { - return {}; - } - return resp.metrics; - }) - .catch(() => { - const prevState = this.state; - this.state = '.js-unable-to-connect'; - this.updateState(prevState); - }); - } - - transformData(metricsResponse) { - Object.keys(metricsResponse.metrics).forEach((key) => { - if (key === 'cpu_values' || key === 'memory_values') { - const metricValues = (metricsResponse.metrics[key])[0]; - this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ - time: new Date(metric[0] * 1000), - value: metric[1], - })); - } - }); - } - - updateState(prevState) { - const $statesContainer = $(prometheusStatesContainer); - $(prometheusParentGraphContainer).hide(); - if (prevState) { - $(`${prevState}`, $statesContainer).addClass('hidden'); - } - $(`${this.state}`, $statesContainer).removeClass('hidden'); - $(prometheusStatesContainer).show(); - } -} - -export default PrometheusGraph; diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js new file mode 100644 index 00000000000..1e9ae934853 --- /dev/null +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -0,0 +1,19 @@ +import Vue from 'vue'; +import VueResource from 'vue-resource'; + +Vue.use(VueResource); + +export default class MonitoringService { + constructor(endpoint) { + this.graphs = Vue.resource(endpoint); + } + + get() { + return this.graphs.get(); + } + + // eslint-disable-next-line class-methods-use-this + getDeploymentData(endpoint) { + return Vue.http.get(endpoint); + } +} diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js new file mode 100644 index 00000000000..737c964f12e --- /dev/null +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -0,0 +1,61 @@ +import _ from 'underscore'; + +class MonitoringStore { + constructor() { + this.groups = []; + this.deploymentData = []; + } + + // eslint-disable-next-line class-methods-use-this + createArrayRows(metrics = []) { + const currentMetrics = metrics; + const availableMetrics = []; + let metricsRow = []; + let index = 1; + Object.keys(currentMetrics).forEach((key) => { + const metricValues = currentMetrics[key].queries[0].result[0].values; + if (metricValues != null) { + const literalMetrics = metricValues.map(metric => ({ + time: new Date(metric[0] * 1000), + value: metric[1], + })); + currentMetrics[key].queries[0].result[0].values = literalMetrics; + metricsRow.push(currentMetrics[key]); + if (index % 2 === 0) { + availableMetrics.push(metricsRow); + metricsRow = []; + } + index = index += 1; + } + }); + if (metricsRow.length > 0) { + availableMetrics.push(metricsRow); + } + return availableMetrics; + } + + storeMetrics(groups = []) { + this.groups = groups.map((group) => { + const currentGroup = group; + currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value(); + currentGroup.metrics = this.createArrayRows(currentGroup.metrics); + return currentGroup; + }); + } + + storeDeploymentData(deploymentData = []) { + this.deploymentData = deploymentData; + } + + getMetricsCount() { + let metricsCount = 0; + this.groups.forEach((group) => { + group.metrics.forEach((metric) => { + metricsCount = metricsCount += metric.length; + }); + }); + return metricsCount; + } +} + +export default MonitoringStore; diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js new file mode 100644 index 00000000000..62cd19c86e1 --- /dev/null +++ b/app/assets/javascripts/monitoring/utils/measurements.js @@ -0,0 +1,40 @@ +export default { + small: { // Covers both xs and sm screen sizes + margin: { + top: 40, + right: 40, + bottom: 50, + left: 40, + }, + legends: { + width: 15, + height: 25, + }, + backgroundLegend: { + width: 30, + height: 50, + }, + axisLabelLineOffset: -20, + legendOffset: 35, + }, + large: { // This covers both md and lg screen sizes + margin: { + top: 80, + right: 80, + bottom: 100, + left: 80, + }, + legends: { + width: 20, + height: 30, + }, + backgroundLegend: { + width: 30, + height: 150, + }, + axisLabelLineOffset: 20, + legendOffset: 38, + }, + xTicks: 8, + yTicks: 3, +}; diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js new file mode 100644 index 00000000000..5f98aff8ced --- /dev/null +++ b/app/assets/javascripts/new_sidebar.js @@ -0,0 +1,23 @@ +export default class NewNavSidebar { + constructor() { + this.initDomElements(); + } + + initDomElements() { + this.$sidebar = $('.nav-sidebar'); + this.$overlay = $('.mobile-overlay'); + this.$openSidebar = $('.toggle-mobile-nav'); + this.$closeSidebar = $('.close-nav-button'); + } + + bindEvents() { + this.$openSidebar.on('click', () => this.toggleSidebarNav(true)); + this.$closeSidebar.on('click', () => this.toggleSidebarNav(false)); + this.$overlay.on('click', () => this.toggleSidebarNav(false)); + } + + toggleSidebarNav(show) { + this.$sidebar.toggleClass('nav-sidebar-expanded', show); + this.$overlay.toggleClass('mobile-nav-open', show); + } +} diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 624dd336786..dfa07a2def4 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -4,7 +4,7 @@ no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, no-sequences, no-shadow, -newline-per-chained-call, no-useless-escape */ +newline-per-chained-call, no-useless-escape, class-methods-use-this */ /* global Flash */ /* global Autosave */ /* global ResolveService */ @@ -18,1514 +18,1513 @@ import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; import CommentTypeToggle from './comment_type_toggle'; +import loadAwardsHandler from './awards_handler'; import './autosave'; import './dropzone_input'; -import './task_list'; +import TaskList from './task_list'; window.autosize = autosize; window.Dropzone = Dropzone; -const normalizeNewlines = function(str) { +function normalizeNewlines(str) { return str.replace(/\r\n/g, '\n'); -}; - -(function() { - this.Notes = (function() { - const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; - const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; - - Notes.interval = null; - - function Notes(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { - this.updateTargetButtons = this.updateTargetButtons.bind(this); - this.updateComment = this.updateComment.bind(this); - this.visibilityChange = this.visibilityChange.bind(this); - this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this); - this.onAddDiffNote = this.onAddDiffNote.bind(this); - this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this); - this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this); - this.removeNote = this.removeNote.bind(this); - this.cancelEdit = this.cancelEdit.bind(this); - this.updateNote = this.updateNote.bind(this); - this.addDiscussionNote = this.addDiscussionNote.bind(this); - this.addNoteError = this.addNoteError.bind(this); - this.addNote = this.addNote.bind(this); - this.resetMainTargetForm = this.resetMainTargetForm.bind(this); - this.refresh = this.refresh.bind(this); - this.keydownNoteText = this.keydownNoteText.bind(this); - this.toggleCommitList = this.toggleCommitList.bind(this); - this.postComment = this.postComment.bind(this); - this.clearFlashWrapper = this.clearFlash.bind(this); - this.onHashChange = this.onHashChange.bind(this); - - this.notes_url = notes_url; - this.note_ids = note_ids; - this.enableGFM = enableGFM; - // Used to keep track of updated notes while people are editing things - this.updatedNotesTrackingMap = {}; - this.last_fetched_at = last_fetched_at; - this.noteable_url = document.URL; - this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge')); - this.basePollingInterval = 15000; - this.maxPollingSteps = 4; - - this.cleanBinding(); - this.addBinding(); - this.setPollingInterval(); - this.setupMainTargetNoteForm(); - this.taskList = new gl.TaskList({ - dataType: 'note', - fieldName: 'note', - selector: '.notes' - }); - this.collapseLongCommitList(); - this.setViewType(view); - - // We are in the Merge Requests page so we need another edit form for Changes tab - if (gl.utils.getPagePath(1) === 'merge_requests') { - $('.note-edit-form').clone() - .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); - } +} + +const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; +const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; + +export default class Notes { + constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { + this.updateTargetButtons = this.updateTargetButtons.bind(this); + this.updateComment = this.updateComment.bind(this); + this.visibilityChange = this.visibilityChange.bind(this); + this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this); + this.onAddDiffNote = this.onAddDiffNote.bind(this); + this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this); + this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this); + this.removeNote = this.removeNote.bind(this); + this.cancelEdit = this.cancelEdit.bind(this); + this.updateNote = this.updateNote.bind(this); + this.addDiscussionNote = this.addDiscussionNote.bind(this); + this.addNoteError = this.addNoteError.bind(this); + this.addNote = this.addNote.bind(this); + this.resetMainTargetForm = this.resetMainTargetForm.bind(this); + this.refresh = this.refresh.bind(this); + this.keydownNoteText = this.keydownNoteText.bind(this); + this.toggleCommitList = this.toggleCommitList.bind(this); + this.postComment = this.postComment.bind(this); + this.clearFlashWrapper = this.clearFlash.bind(this); + this.onHashChange = this.onHashChange.bind(this); + + this.notes_url = notes_url; + this.note_ids = note_ids; + this.enableGFM = enableGFM; + // Used to keep track of updated notes while people are editing things + this.updatedNotesTrackingMap = {}; + this.last_fetched_at = last_fetched_at; + this.noteable_url = document.URL; + this.notesCountBadge || (this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge')); + this.basePollingInterval = 15000; + this.maxPollingSteps = 4; + + this.cleanBinding(); + this.addBinding(); + this.setPollingInterval(); + this.setupMainTargetNoteForm(); + this.taskList = new TaskList({ + dataType: 'note', + fieldName: 'note', + selector: '.notes' + }); + this.collapseLongCommitList(); + this.setViewType(view); + + // We are in the Merge Requests page so we need another edit form for Changes tab + if (gl.utils.getPagePath(1) === 'merge_requests') { + $('.note-edit-form').clone() + .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); + } + } + + setViewType(view) { + this.view = Cookies.get('diff_view') || view; + } + + addBinding() { + // Edit note link + $(document).on('click', '.js-note-edit', this.showEditForm.bind(this)); + $(document).on('click', '.note-edit-cancel', this.cancelEdit); + // Reopen and close actions for Issue/MR combined with note form submit + $(document).on('click', '.js-comment-submit-button', this.postComment); + $(document).on('click', '.js-comment-save-button', this.updateComment); + $(document).on('keyup input', '.js-note-text', this.updateTargetButtons); + // resolve a discussion + $(document).on('click', '.js-comment-resolve-button', this.postComment); + // remove a note (in general) + $(document).on('click', '.js-note-delete', this.removeNote); + // delete note attachment + $(document).on('click', '.js-note-attachment-delete', this.removeAttachment); + // reset main target form when clicking discard + $(document).on('click', '.js-note-discard', this.resetMainTargetForm); + // update the file name when an attachment is selected + $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment); + // reply to diff/discussion notes + $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); + // add diff note + $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote); + // hide diff note form + $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); + // toggle commit list + $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList); + // fetch notes when tab becomes visible + $(document).on('visibilitychange', this.visibilityChange); + // when issue status changes, we need to refresh data + $(document).on('issuable:change', this.refresh); + // ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs. + $(document).on('ajax:success', '.js-main-target-form', this.addNote); + $(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote); + $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); + $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); + // when a key is clicked on the notes + $(document).on('keydown', '.js-note-text', this.keydownNoteText); + // When the URL fragment/hash has changed, `#note_xxx` + return $(window).on('hashchange', this.onHashChange); + } + + cleanBinding() { + $(document).off('click', '.js-note-edit'); + $(document).off('click', '.note-edit-cancel'); + $(document).off('click', '.js-note-delete'); + $(document).off('click', '.js-note-attachment-delete'); + $(document).off('click', '.js-discussion-reply-button'); + $(document).off('click', '.js-add-diff-note-button'); + $(document).off('visibilitychange'); + $(document).off('keyup input', '.js-note-text'); + $(document).off('click', '.js-note-target-reopen'); + $(document).off('click', '.js-note-target-close'); + $(document).off('click', '.js-note-discard'); + $(document).off('keydown', '.js-note-text'); + $(document).off('click', '.js-comment-resolve-button'); + $(document).off('click', '.system-note-commit-list-toggler'); + $(document).off('ajax:success', '.js-main-target-form'); + $(document).off('ajax:success', '.js-discussion-note-form'); + $(document).off('ajax:complete', '.js-main-target-form'); + $(window).off('hashchange', this.onHashChange); + } + + static initCommentTypeToggle(form) { + const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle'); + const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu'); + const noteTypeInput = form.querySelector('#note_type'); + const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button'); + const closeButton = form.querySelector('.js-note-target-close'); + const reopenButton = form.querySelector('.js-note-target-reopen'); + + const commentTypeToggle = new CommentTypeToggle({ + dropdownTrigger, + dropdownList, + noteTypeInput, + submitButton, + closeButton, + reopenButton, + }); + + commentTypeToggle.initDroplab(); + } + + keydownNoteText(e) { + var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; + if (gl.utils.isMetaKey(e)) { + return; } - Notes.prototype.setViewType = function(view) { - this.view = Cookies.get('diff_view') || view; - }; - - Notes.prototype.addBinding = function() { - // Edit note link - $(document).on('click', '.js-note-edit', this.showEditForm.bind(this)); - $(document).on('click', '.note-edit-cancel', this.cancelEdit); - // Reopen and close actions for Issue/MR combined with note form submit - $(document).on('click', '.js-comment-submit-button', this.postComment); - $(document).on('click', '.js-comment-save-button', this.updateComment); - $(document).on('keyup input', '.js-note-text', this.updateTargetButtons); - // resolve a discussion - $(document).on('click', '.js-comment-resolve-button', this.postComment); - // remove a note (in general) - $(document).on('click', '.js-note-delete', this.removeNote); - // delete note attachment - $(document).on('click', '.js-note-attachment-delete', this.removeAttachment); - // reset main target form when clicking discard - $(document).on('click', '.js-note-discard', this.resetMainTargetForm); - // update the file name when an attachment is selected - $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment); - // reply to diff/discussion notes - $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote); - // add diff note - $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote); - // hide diff note form - $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm); - // toggle commit list - $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList); - // fetch notes when tab becomes visible - $(document).on('visibilitychange', this.visibilityChange); - // when issue status changes, we need to refresh data - $(document).on('issuable:change', this.refresh); - // ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs. - $(document).on('ajax:success', '.js-main-target-form', this.addNote); - $(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote); - $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm); - $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton); - // when a key is clicked on the notes - $(document).on('keydown', '.js-note-text', this.keydownNoteText); - // When the URL fragment/hash has changed, `#note_xxx` - return $(window).on('hashchange', this.onHashChange); - }; - - Notes.prototype.cleanBinding = function() { - $(document).off('click', '.js-note-edit'); - $(document).off('click', '.note-edit-cancel'); - $(document).off('click', '.js-note-delete'); - $(document).off('click', '.js-note-attachment-delete'); - $(document).off('click', '.js-discussion-reply-button'); - $(document).off('click', '.js-add-diff-note-button'); - $(document).off('visibilitychange'); - $(document).off('keyup input', '.js-note-text'); - $(document).off('click', '.js-note-target-reopen'); - $(document).off('click', '.js-note-target-close'); - $(document).off('click', '.js-note-discard'); - $(document).off('keydown', '.js-note-text'); - $(document).off('click', '.js-comment-resolve-button'); - $(document).off('click', '.system-note-commit-list-toggler'); - $(document).off('ajax:success', '.js-main-target-form'); - $(document).off('ajax:success', '.js-discussion-note-form'); - $(document).off('ajax:complete', '.js-main-target-form'); - $(window).off('hashchange', this.onHashChange); - }; - - Notes.initCommentTypeToggle = function (form) { - const dropdownTrigger = form.querySelector('.js-comment-type-dropdown .dropdown-toggle'); - const dropdownList = form.querySelector('.js-comment-type-dropdown .dropdown-menu'); - const noteTypeInput = form.querySelector('#note_type'); - const submitButton = form.querySelector('.js-comment-type-dropdown .js-comment-submit-button'); - const closeButton = form.querySelector('.js-note-target-close'); - const reopenButton = form.querySelector('.js-note-target-reopen'); - - const commentTypeToggle = new CommentTypeToggle({ - dropdownTrigger, - dropdownList, - noteTypeInput, - submitButton, - closeButton, - reopenButton, - }); - - commentTypeToggle.initDroplab(); - }; - - Notes.prototype.keydownNoteText = function(e) { - var $textarea, discussionNoteForm, editNote, myLastNote, myLastNoteEditBtn, newText, originalText; - if (gl.utils.isMetaKey(e)) { - return; - } - - $textarea = $(e.target); - // Edit previous note when UP arrow is hit - switch (e.which) { - case 38: + $textarea = $(e.target); + // Edit previous note when UP arrow is hit + switch (e.which) { + case 38: + if ($textarea.val() !== '') { + return; + } + myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, .notes_holder, #notes')); + if (myLastNote.length) { + myLastNoteEditBtn = myLastNote.find('.js-note-edit'); + return myLastNoteEditBtn.trigger('click', [true, myLastNote]); + } + break; + // Cancel creating diff note or editing any note when ESCAPE is hit + case 27: + discussionNoteForm = $textarea.closest('.js-discussion-note-form'); + if (discussionNoteForm.length) { if ($textarea.val() !== '') { - return; - } - myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, .notes_holder, #notes')); - if (myLastNote.length) { - myLastNoteEditBtn = myLastNote.find('.js-note-edit'); - return myLastNoteEditBtn.trigger('click', [true, myLastNote]); - } - break; - // Cancel creating diff note or editing any note when ESCAPE is hit - case 27: - discussionNoteForm = $textarea.closest('.js-discussion-note-form'); - if (discussionNoteForm.length) { - if ($textarea.val() !== '') { - if (!confirm('Are you sure you want to cancel creating this comment?')) { - return; - } + if (!confirm('Are you sure you want to cancel creating this comment?')) { + return; } - this.removeDiscussionNoteForm(discussionNoteForm); - return; } - editNote = $textarea.closest('.note'); - if (editNote.length) { - originalText = $textarea.closest('form').data('original-note'); - newText = $textarea.val(); - if (originalText !== newText) { - if (!confirm('Are you sure you want to cancel editing this comment?')) { - return; - } + this.removeDiscussionNoteForm(discussionNoteForm); + return; + } + editNote = $textarea.closest('.note'); + if (editNote.length) { + originalText = $textarea.closest('form').data('original-note'); + newText = $textarea.val(); + if (originalText !== newText) { + if (!confirm('Are you sure you want to cancel editing this comment?')) { + return; } - return this.removeNoteEditForm(editNote); } - } - }; + return this.removeNoteEditForm(editNote); + } + } + } - Notes.prototype.initRefresh = function() { + initRefresh() { + if (Notes.interval) { clearInterval(Notes.interval); - return Notes.interval = setInterval((function(_this) { - return function() { - return _this.refresh(); - }; - })(this), this.pollingInterval); - }; + } + return Notes.interval = setInterval((function(_this) { + return function() { + return _this.refresh(); + }; + })(this), this.pollingInterval); + } - Notes.prototype.refresh = function() { - if (!document.hidden) { - return this.getContent(); - } - }; + refresh() { + if (!document.hidden) { + return this.getContent(); + } + } - Notes.prototype.getContent = function() { - if (this.refreshing) { - return; - } - this.refreshing = true; - return $.ajax({ - url: this.notes_url, - headers: { 'X-Last-Fetched-At': this.last_fetched_at }, - dataType: 'json', - success: (function(_this) { - return function(data) { - var notes; - notes = data.notes; - _this.last_fetched_at = data.last_fetched_at; - _this.setPollingInterval(data.notes.length); - return $.each(notes, function(i, note) { - _this.renderNote(note); - }); - }; - })(this) - }).always((function(_this) { - return function() { - return _this.refreshing = false; + getContent() { + if (this.refreshing) { + return; + } + this.refreshing = true; + return $.ajax({ + url: this.notes_url, + headers: { 'X-Last-Fetched-At': this.last_fetched_at }, + dataType: 'json', + success: (function(_this) { + return function(data) { + var notes; + notes = data.notes; + _this.last_fetched_at = data.last_fetched_at; + _this.setPollingInterval(data.notes.length); + return $.each(notes, function(i, note) { + _this.renderNote(note); + }); }; - })(this)); - }; - - /* - Increase @pollingInterval up to 120 seconds on every function call, - if `shouldReset` has a truthy value, 'null' or 'undefined' the variable - will reset to @basePollingInterval. - - Note: this function is used to gradually increase the polling interval - if there aren't new notes coming from the server - */ - - Notes.prototype.setPollingInterval = function(shouldReset) { - var nthInterval; - if (shouldReset == null) { - shouldReset = true; - } - nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); - if (shouldReset) { - this.pollingInterval = this.basePollingInterval; - } else if (this.pollingInterval < nthInterval) { - this.pollingInterval *= 2; - } - return this.initRefresh(); - }; - - Notes.prototype.handleQuickActions = function(noteEntity) { - var votesBlock; - if (noteEntity.commands_changes) { - if ('merge' in noteEntity.commands_changes) { - Notes.checkMergeRequestStatus(); - } - - if ('emoji_award' in noteEntity.commands_changes) { - votesBlock = $('.js-awards-block').eq(0); - gl.awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); - return gl.awardsHandler.scrollToAwards(); - } + })(this) + }).always((function(_this) { + return function() { + return _this.refreshing = false; + }; + })(this)); + } + + /** + * Increase @pollingInterval up to 120 seconds on every function call, + * if `shouldReset` has a truthy value, 'null' or 'undefined' the variable + * will reset to @basePollingInterval. + * + * Note: this function is used to gradually increase the polling interval + * if there aren't new notes coming from the server + */ + setPollingInterval(shouldReset) { + var nthInterval; + if (shouldReset == null) { + shouldReset = true; + } + nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1); + if (shouldReset) { + this.pollingInterval = this.basePollingInterval; + } else if (this.pollingInterval < nthInterval) { + this.pollingInterval *= 2; + } + return this.initRefresh(); + } + + handleQuickActions(noteEntity) { + var votesBlock; + if (noteEntity.commands_changes) { + if ('merge' in noteEntity.commands_changes) { + Notes.checkMergeRequestStatus(); } - }; - - Notes.prototype.setupNewNote = function($note) { - // Update datetime format on the recent note - gl.utils.localTimeAgo($note.find('.js-timeago'), false); - this.collapseLongCommitList(); - this.taskList.init(); + if ('emoji_award' in noteEntity.commands_changes) { + votesBlock = $('.js-awards-block').eq(0); - // This stops the note highlight, #note_xxx`, from being removed after real time update - // The `:target` selector does not re-evaluate after we replace element in the DOM - Notes.updateNoteTargetSelector($note); - this.$noteToCleanHighlight = $note; - }; - - Notes.prototype.onHashChange = function() { - if (this.$noteToCleanHighlight) { - Notes.updateNoteTargetSelector(this.$noteToCleanHighlight); + loadAwardsHandler().then((awardsHandler) => { + awardsHandler.addAwardToEmojiBar(votesBlock, noteEntity.commands_changes.emoji_award); + awardsHandler.scrollToAwards(); + }).catch(() => { + // ignore + }); } + } + } - this.$noteToCleanHighlight = null; - }; - - Notes.updateNoteTargetSelector = function($note) { - const hash = gl.utils.getLocationHash(); - // Needs to be an explicit true/false for the jQuery `toggleClass(force)` - const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0); - $note.toggleClass('target', addTargetClass); - }; + setupNewNote($note) { + // Update datetime format on the recent note + gl.utils.localTimeAgo($note.find('.js-timeago'), false); - /* - Render note in main comments area. + this.collapseLongCommitList(); + this.taskList.init(); - Note: for rendering inline notes use renderDiscussionNote - */ + // This stops the note highlight, #note_xxx`, from being removed after real time update + // The `:target` selector does not re-evaluate after we replace element in the DOM + Notes.updateNoteTargetSelector($note); + this.$noteToCleanHighlight = $note; + } - Notes.prototype.renderNote = function(noteEntity, $form, $notesList = $('.main-notes-list')) { - if (noteEntity.discussion_html) { - return this.renderDiscussionNote(noteEntity, $form); - } - - if (!noteEntity.valid) { - if (noteEntity.errors.commands_only) { - this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); - this.refresh(); - } - return; - } + onHashChange() { + if (this.$noteToCleanHighlight) { + Notes.updateNoteTargetSelector(this.$noteToCleanHighlight); + } - const $note = $notesList.find(`#note_${noteEntity.id}`); - if (Notes.isNewNote(noteEntity, this.note_ids)) { - this.note_ids.push(noteEntity.id); + this.$noteToCleanHighlight = null; + } + + static updateNoteTargetSelector($note) { + const hash = gl.utils.getLocationHash(); + // Needs to be an explicit true/false for the jQuery `toggleClass(force)` + const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0); + $note.toggleClass('target', addTargetClass); + } + + /** + * Render note in main comments area. + * + * Note: for rendering inline notes use renderDiscussionNote + */ + renderNote(noteEntity, $form, $notesList = $('.main-notes-list')) { + if (noteEntity.discussion_html) { + return this.renderDiscussionNote(noteEntity, $form); + } - if ($notesList.length) { + if (!noteEntity.valid) { + if (noteEntity.errors.commands_only) { + if (noteEntity.commands_changes && + Object.keys(noteEntity.commands_changes).length > 0) { $notesList.find('.system-note.being-posted').remove(); } - const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList); - - this.setupNewNote($newNote); + this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); this.refresh(); - return this.updateNotesCount(1); } - // The server can send the same update multiple times so we need to make sure to only update once per actual update. - else if (Notes.isUpdatedNote(noteEntity, $note)) { - const isEditing = $note.hasClass('is-editing'); - const initialContent = normalizeNewlines( - $note.find('.original-note-content').text().trim() - ); - const $textarea = $note.find('.js-note-text'); - const currentContent = $textarea.val(); - // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way - const sanitizedNoteNote = normalizeNewlines(noteEntity.note); - const isTextareaUntouched = currentContent === initialContent || currentContent === sanitizedNoteNote; - - if (isEditing && isTextareaUntouched) { - $textarea.val(noteEntity.note); - this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; - } - else if (isEditing && !isTextareaUntouched) { - this.putConflictEditWarningInPlace(noteEntity, $note); - this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; - } - else { - const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note); - this.setupNewNote($updatedNote); - } - } - }; - - Notes.prototype.isParallelView = function() { - return Cookies.get('diff_view') === 'parallel'; - }; - - /* - Render note in discussion area. - - Note: for rendering inline notes use renderDiscussionNote - */ + return; + } - Notes.prototype.renderDiscussionNote = function(noteEntity, $form) { - var discussionContainer, form, row, lineType, diffAvatarContainer; - if (!Notes.isNewNote(noteEntity, this.note_ids)) { - return; - } + const $note = $notesList.find(`#note_${noteEntity.id}`); + if (Notes.isNewNote(noteEntity, this.note_ids)) { this.note_ids.push(noteEntity.id); - form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); - row = form.closest('tr'); - lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; - diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); - // is this the first note of discussion? - discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); - if (!discussionContainer.length) { - discussionContainer = form.closest('.discussion').find('.notes'); - } - if (discussionContainer.length === 0) { - if (noteEntity.diff_discussion_html) { - var $discussion = $(noteEntity.diff_discussion_html).renderGFM(); - - if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { - // insert the note and the reply button after the temp row - row.after($discussion); - } else { - // Merge new discussion HTML in - var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); - var contentContainerClass = '.' + $notes.closest('.notes_content') - .attr('class') - .split(' ') - .join('.'); - - row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); - } - } - // Init discussion on 'Discussion' page if it is merge request page - const page = $('body').attr('data-page'); - if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) { - Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list')); - } - } else { - // append new note to all matching discussions - Notes.animateAppendNote(noteEntity.html, discussionContainer); - } - if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { - gl.diffNotesCompileComponents(); - this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); + if ($notesList.length) { + $notesList.find('.system-note.being-posted').remove(); } + const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList); - gl.utils.localTimeAgo($('.js-timeago'), false); - Notes.checkMergeRequestStatus(); + this.setupNewNote($newNote); + this.refresh(); return this.updateNotesCount(1); - }; - - Notes.prototype.getLineHolder = function(changesDiscussionContainer) { - return $(changesDiscussionContainer).closest('.notes_holder') - .prevAll('.line_holder') - .first() - .get(0); - }; - - Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, noteEntity) { - var commentButton = diffAvatarContainer.find('.js-add-diff-note-button'); - var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); - - if (!avatarHolder.length) { - avatarHolder = document.createElement('diff-note-avatars'); - avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id); - - diffAvatarContainer.append(avatarHolder); + } + // The server can send the same update multiple times so we need to make sure to only update once per actual update. + else if (Notes.isUpdatedNote(noteEntity, $note)) { + const isEditing = $note.hasClass('is-editing'); + const initialContent = normalizeNewlines( + $note.find('.original-note-content').text().trim() + ); + const $textarea = $note.find('.js-note-text'); + const currentContent = $textarea.val(); + // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way + const sanitizedNoteNote = normalizeNewlines(noteEntity.note); + const isTextareaUntouched = currentContent === initialContent || currentContent === sanitizedNoteNote; - gl.diffNotesCompileComponents(); + if (isEditing && isTextareaUntouched) { + $textarea.val(noteEntity.note); + this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; } - - if (commentButton.length) { - commentButton.remove(); + else if (isEditing && !isTextareaUntouched) { + this.putConflictEditWarningInPlace(noteEntity, $note); + this.updatedNotesTrackingMap[noteEntity.id] = noteEntity; } - }; - - /* - Called in response the main target form has been successfully submitted. - - Removes any errors. - Resets text and preview. - Resets buttons. - */ - - Notes.prototype.resetMainTargetForm = function(e) { - var form; - form = $('.js-main-target-form'); - // remove validation errors - form.find('.js-errors').remove(); - // reset text and preview - form.find('.js-md-write-button').click(); - form.find('.js-note-text').val('').trigger('input'); - form.find('.js-note-text').data('autosave').reset(); - - var event = document.createEvent('Event'); - event.initEvent('autosize:update', true, false); - form.find('.js-autosize')[0].dispatchEvent(event); - - this.updateTargetButtons(e); - }; - - Notes.prototype.reenableTargetFormSubmitButton = function() { - var form; - form = $('.js-main-target-form'); - return form.find('.js-note-text').trigger('input'); - }; - - /* - Shows the main form and does some setup on it. - - Sets some hidden fields in the form. - */ - - Notes.prototype.setupMainTargetNoteForm = function() { - var form; - // find the form - form = $('.js-new-note-form'); - // Set a global clone of the form for later cloning - this.formClone = form.clone(); - // show the form - this.setupNoteForm(form); - // fix classes - form.removeClass('js-new-note-form'); - form.addClass('js-main-target-form'); - form.find('#note_line_code').remove(); - form.find('#note_position').remove(); - form.find('#note_type').val(''); - form.find('#in_reply_to_discussion_id').remove(); - form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); - this.parentTimeline = form.parents('.timeline'); - - if (form.length) { - Notes.initCommentTypeToggle(form.get(0)); - } - }; - - /* - General note form setup. - - deactivates the submit button when text is empty - hides the preview button when text is empty - setup GFM auto complete - show the form - */ - - Notes.prototype.setupNoteForm = function(form) { - var textarea, key; - new gl.GLForm(form, this.enableGFM); - textarea = form.find('.js-note-text'); - key = [ - 'Note', - form.find('#note_noteable_type').val(), - form.find('#note_noteable_id').val(), - form.find('#note_commit_id').val(), - form.find('#note_type').val(), - form.find('#in_reply_to_discussion_id').val(), - - // LegacyDiffNote - form.find('#note_line_code').val(), - - // DiffNote - form.find('#note_position').val() - ]; - return new Autosave(textarea, key); - }; - - /* - Called in response to the new note form being submitted - - Adds new note to list. - */ - - Notes.prototype.addNote = function($form, note) { - return this.renderNote(note); - }; - - Notes.prototype.addNoteError = function($form) { - let formParentTimeline; - if ($form.hasClass('js-main-target-form')) { - formParentTimeline = $form.parents('.timeline'); - } else if ($form.hasClass('js-discussion-note-form')) { - formParentTimeline = $form.closest('.discussion-notes').find('.notes'); + else { + const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note); + this.setupNewNote($updatedNote); } - return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline); - }; - - Notes.prototype.updateNoteError = $parentTimeline => new Flash('Your comment could not be updated! Please check your network connection and try again.'); - - /* - Called in response to the new note form being submitted - - Adds new note to list. - */ - - Notes.prototype.addDiscussionNote = function($form, note, isNewDiffComment) { - if ($form.attr('data-resolve-all') != null) { - var projectPath = $form.data('project-path'); - var discussionId = $form.data('discussion-id'); - var mergeRequestId = $form.data('noteable-iid'); + } + } + + isParallelView() { + return Cookies.get('diff_view') === 'parallel'; + } + + /** + * Render note in discussion area. + * + * Note: for rendering inline notes use renderDiscussionNote + */ + renderDiscussionNote(noteEntity, $form) { + var discussionContainer, form, row, lineType, diffAvatarContainer; + if (!Notes.isNewNote(noteEntity, this.note_ids)) { + return; + } + this.note_ids.push(noteEntity.id); + form = $form || $(`.js-discussion-note-form[data-discussion-id="${noteEntity.discussion_id}"]`); + row = form.closest('tr'); + lineType = this.isParallelView() ? form.find('#line_type').val() : 'old'; + diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line'); + // is this the first note of discussion? + discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); + if (!discussionContainer.length) { + discussionContainer = form.closest('.discussion').find('.notes'); + } + if (discussionContainer.length === 0) { + if (noteEntity.diff_discussion_html) { + var $discussion = $(noteEntity.diff_discussion_html).renderGFM(); - if (ResolveService != null) { - ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId); + if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) { + // insert the note and the reply button after the temp row + row.after($discussion); + } else { + // Merge new discussion HTML in + var $notes = $discussion.find(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`); + var contentContainerClass = '.' + $notes.closest('.notes_content') + .attr('class') + .split(' ') + .join('.'); + + row.find(contentContainerClass + ' .content').append($notes.closest('.content').children()); } } - - this.renderNote(note, $form); - // cleanup after successfully creating a diff/discussion note - if (isNewDiffComment) { - this.removeDiscussionNoteForm($form); + // Init discussion on 'Discussion' page if it is merge request page + const page = $('body').attr('data-page'); + if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) { + Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list')); } - }; + } else { + // append new note to all matching discussions + Notes.animateAppendNote(noteEntity.html, discussionContainer); + } - /* - Called in response to the edit note form being submitted + if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) { + gl.diffNotesCompileComponents(); + this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); + } - Updates the current note field. - */ + gl.utils.localTimeAgo($('.js-timeago'), false); + Notes.checkMergeRequestStatus(); + return this.updateNotesCount(1); + } - Notes.prototype.updateNote = function(noteEntity, $targetNote) { - var $noteEntityEl, $note_li; - // Convert returned HTML to a jQuery object so we can modify it further - $noteEntityEl = $(noteEntity.html); - $noteEntityEl.addClass('fade-in-full'); - this.revertNoteEditForm($targetNote); - $noteEntityEl.renderGFM(); - // Find the note's `li` element by ID and replace it with the updated HTML - $note_li = $('.note-row-' + noteEntity.id); + getLineHolder(changesDiscussionContainer) { + return $(changesDiscussionContainer).closest('.notes_holder') + .prevAll('.line_holder') + .first() + .get(0); + } - $note_li.replaceWith($noteEntityEl); - this.setupNewNote($noteEntityEl); + renderDiscussionAvatar(diffAvatarContainer, noteEntity) { + var commentButton = diffAvatarContainer.find('.js-add-diff-note-button'); + var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } - }; + if (!avatarHolder.length) { + avatarHolder = document.createElement('diff-note-avatars'); + avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id); - Notes.prototype.checkContentToAllowEditing = function($el) { - var initialContent = $el.find('.original-note-content').text().trim(); - var currentContent = $el.find('.js-note-text').val(); - var isAllowed = true; + diffAvatarContainer.append(avatarHolder); - if (currentContent === initialContent) { - this.removeNoteEditForm($el); - } - else { - var $buttons = $el.find('.note-form-actions'); - var isWidgetVisible = gl.utils.isInViewport($el.get(0)); - - if (!isWidgetVisible) { - gl.utils.scrollToElement($el); - } + gl.diffNotesCompileComponents(); + } - $el.find('.js-finish-edit-warning').show(); - isAllowed = false; - } + if (commentButton.length) { + commentButton.remove(); + } + } + + /** + * Called in response the main target form has been successfully submitted. + * + * Removes any errors. + * Resets text and preview. + * Resets buttons. + */ + resetMainTargetForm(e) { + var form; + form = $('.js-main-target-form'); + // remove validation errors + form.find('.js-errors').remove(); + // reset text and preview + form.find('.js-md-write-button').click(); + form.find('.js-note-text').val('').trigger('input'); + form.find('.js-note-text').data('autosave').reset(); + + var event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + form.find('.js-autosize')[0].dispatchEvent(event); + + this.updateTargetButtons(e); + } + + reenableTargetFormSubmitButton() { + var form; + form = $('.js-main-target-form'); + return form.find('.js-note-text').trigger('input'); + } + + /** + * Shows the main form and does some setup on it. + * + * Sets some hidden fields in the form. + */ + setupMainTargetNoteForm() { + var form; + // find the form + form = $('.js-new-note-form'); + // Set a global clone of the form for later cloning + this.formClone = form.clone(); + // show the form + this.setupNoteForm(form); + // fix classes + form.removeClass('js-new-note-form'); + form.addClass('js-main-target-form'); + form.find('#note_line_code').remove(); + form.find('#note_position').remove(); + form.find('#note_type').val(''); + form.find('#note_project_id').remove(); + form.find('#in_reply_to_discussion_id').remove(); + form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove(); + this.parentTimeline = form.parents('.timeline'); + + if (form.length) { + Notes.initCommentTypeToggle(form.get(0)); + } + } + + /** + * General note form setup. + * + * deactivates the submit button when text is empty + * hides the preview button when text is empty + * setup GFM auto complete + * show the form + */ + setupNoteForm(form) { + var textarea, key; + new gl.GLForm(form, this.enableGFM); + textarea = form.find('.js-note-text'); + key = [ + 'Note', + form.find('#note_noteable_type').val(), + form.find('#note_noteable_id').val(), + form.find('#note_commit_id').val(), + form.find('#note_type').val(), + form.find('#note_project_id').val(), + form.find('#in_reply_to_discussion_id').val(), - return isAllowed; - }; + // LegacyDiffNote + form.find('#note_line_code').val(), - /* - Called in response to clicking the edit note link + // DiffNote + form.find('#note_position').val() + ]; + return new Autosave(textarea, key); + } + + /** + * Called in response to the new note form being submitted + * + * Adds new note to list. + */ + addNote($form, note) { + return this.renderNote(note); + } + + addNoteError($form) { + let formParentTimeline; + if ($form.hasClass('js-main-target-form')) { + formParentTimeline = $form.parents('.timeline'); + } else if ($form.hasClass('js-discussion-note-form')) { + formParentTimeline = $form.closest('.discussion-notes').find('.notes'); + } + return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline); + } + + updateNoteError($parentTimeline) { + new Flash('Your comment could not be updated! Please check your network connection and try again.'); + } + + /** + * Called in response to the new note form being submitted + * + * Adds new note to list. + */ + addDiscussionNote($form, note, isNewDiffComment) { + if ($form.attr('data-resolve-all') != null) { + var projectPath = $form.data('project-path'); + var discussionId = $form.data('discussion-id'); + var mergeRequestId = $form.data('noteable-iid'); + + if (ResolveService != null) { + ResolveService.toggleResolveForDiscussion(mergeRequestId, discussionId); + } + } - Replaces the note text with the note edit form - Adds a data attribute to the form with the original content of the note for cancellations - */ - Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) { - e.preventDefault(); + this.renderNote(note, $form); + // cleanup after successfully creating a diff/discussion note + if (isNewDiffComment) { + this.removeDiscussionNoteForm($form); + } + } + + /** + * Called in response to the edit note form being submitted + * + * Updates the current note field. + */ + updateNote(noteEntity, $targetNote) { + var $noteEntityEl, $note_li; + // Convert returned HTML to a jQuery object so we can modify it further + $noteEntityEl = $(noteEntity.html); + $noteEntityEl.addClass('fade-in-full'); + this.revertNoteEditForm($targetNote); + $noteEntityEl.renderGFM(); + // Find the note's `li` element by ID and replace it with the updated HTML + $note_li = $('.note-row-' + noteEntity.id); + + $note_li.replaceWith($noteEntityEl); + this.setupNewNote($noteEntityEl); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + } - var $target = $(e.target); - var $editForm = $(this.getEditFormSelector($target)); - var $note = $target.closest('.note'); - var $currentlyEditing = $('.note.is-editing:visible'); + checkContentToAllowEditing($el) { + var initialContent = $el.find('.original-note-content').text().trim(); + var currentContent = $el.find('.js-note-text').val(); + var isAllowed = true; - if ($currentlyEditing.length) { - var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing); + if (currentContent === initialContent) { + this.removeNoteEditForm($el); + } + else { + var $buttons = $el.find('.note-form-actions'); + var isWidgetVisible = gl.utils.isInViewport($el.get(0)); - if (!isEditAllowed) { - return; - } + if (!isWidgetVisible) { + gl.utils.scrollToElement($el); } - $note.find('.js-note-attachment-delete').show(); - $editForm.addClass('current-note-edit-form'); - $note.addClass('is-editing'); - this.putEditFormInPlace($target); - }; + $el.find('.js-finish-edit-warning').show(); + isAllowed = false; + } - /* - Called in response to clicking the edit note link + return isAllowed; + } - Hides edit form and restores the original note text to the editor textarea. - */ + /** + * Called in response to clicking the edit note link + * + * Replaces the note text with the note edit form + * Adds a data attribute to the form with the original content of the note for cancellations + */ + showEditForm(e, scrollTo, myLastNote) { + e.preventDefault(); - Notes.prototype.cancelEdit = function(e) { - e.preventDefault(); - const $target = $(e.target); - const $note = $target.closest('.note'); - const noteId = $note.attr('data-note-id'); + var $target = $(e.target); + var $editForm = $(this.getEditFormSelector($target)); + var $note = $target.closest('.note'); + var $currentlyEditing = $('.note.is-editing:visible'); - this.revertNoteEditForm($target); + if ($currentlyEditing.length) { + var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing); - if (this.updatedNotesTrackingMap[noteId]) { - const $newNote = $(this.updatedNotesTrackingMap[noteId].html); - $note.replaceWith($newNote); - this.setupNewNote($newNote); - // Now that we have taken care of the update, clear it out - delete this.updatedNotesTrackingMap[noteId]; - } - else { - $note.find('.js-finish-edit-warning').hide(); - this.removeNoteEditForm($note); + if (!isEditAllowed) { + return; } - }; - - Notes.prototype.revertNoteEditForm = function($target) { - $target = $target || $('.note.is-editing:visible'); - var selector = this.getEditFormSelector($target); - var $editForm = $(selector); + } - $editForm.insertBefore('.notes-form'); - $editForm.find('.js-comment-save-button').enable(); - $editForm.find('.js-finish-edit-warning').hide(); - }; + $note.find('.js-note-attachment-delete').show(); + $editForm.addClass('current-note-edit-form'); + $note.addClass('is-editing'); + this.putEditFormInPlace($target); + } + + /** + * Called in response to clicking the edit note link + * + * Hides edit form and restores the original note text to the editor textarea. + */ + cancelEdit(e) { + e.preventDefault(); + const $target = $(e.target); + const $note = $target.closest('.note'); + const noteId = $note.attr('data-note-id'); + + this.revertNoteEditForm($target); + + if (this.updatedNotesTrackingMap[noteId]) { + const $newNote = $(this.updatedNotesTrackingMap[noteId].html); + $note.replaceWith($newNote); + this.setupNewNote($newNote); + // Now that we have taken care of the update, clear it out + delete this.updatedNotesTrackingMap[noteId]; + } + else { + $note.find('.js-finish-edit-warning').hide(); + this.removeNoteEditForm($note); + } + } - Notes.prototype.getEditFormSelector = function($el) { - var selector = '.note-edit-form:not(.mr-note-edit-form)'; + revertNoteEditForm($target) { + $target = $target || $('.note.is-editing:visible'); + var selector = this.getEditFormSelector($target); + var $editForm = $(selector); - if ($el.parents('#diffs').length) { - selector = '.note-edit-form.mr-note-edit-form'; - } + $editForm.insertBefore('.notes-form'); + $editForm.find('.js-comment-save-button').enable(); + $editForm.find('.js-finish-edit-warning').hide(); + } - return selector; - }; + getEditFormSelector($el) { + var selector = '.note-edit-form:not(.mr-note-edit-form)'; - Notes.prototype.removeNoteEditForm = function($note) { - var form = $note.find('.current-note-edit-form'); - $note.removeClass('is-editing'); - form.removeClass('current-note-edit-form'); - form.find('.js-finish-edit-warning').hide(); - // Replace markdown textarea text with original note text. - return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note')); - }; + if ($el.parents('#diffs').length) { + selector = '.note-edit-form.mr-note-edit-form'; + } - /* - Called in response to deleting a note of any kind. - - Removes the actual note from view. - Removes the whole discussion if the last note is being removed. - */ - - Notes.prototype.removeNote = function(e) { - var noteElId, noteId, dataNoteId, $note, lineHolder; - $note = $(e.currentTarget).closest('.note'); - noteElId = $note.attr('id'); - noteId = $note.attr('data-note-id'); - lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]') - .closest('.notes_holder') - .prev('.line_holder'); - $(`.note[id="${noteElId}"]`).each((function(_this) { - // A same note appears in the "Discussion" and in the "Changes" tab, we have - // to remove all. Using $('.note[id='noteId']') ensure we get all the notes, - // where $('#noteId') would return only one. - return function(i, el) { - var $note, $notes; - $note = $(el); - $notes = $note.closest('.discussion-notes'); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - if (gl.diffNoteApps[noteElId]) { - gl.diffNoteApps[noteElId].$destroy(); - } + return selector; + } + + removeNoteEditForm($note) { + var form = $note.find('.current-note-edit-form'); + $note.removeClass('is-editing'); + form.removeClass('current-note-edit-form'); + form.find('.js-finish-edit-warning').hide(); + // Replace markdown textarea text with original note text. + return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note')); + } + + /** + * Called in response to deleting a note of any kind. + * + * Removes the actual note from view. + * Removes the whole discussion if the last note is being removed. + */ + removeNote(e) { + var noteElId, noteId, dataNoteId, $note, lineHolder; + $note = $(e.currentTarget).closest('.note'); + noteElId = $note.attr('id'); + noteId = $note.attr('data-note-id'); + lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]') + .closest('.notes_holder') + .prev('.line_holder'); + $(`.note[id="${noteElId}"]`).each((function(_this) { + // A same note appears in the "Discussion" and in the "Changes" tab, we have + // to remove all. Using $('.note[id='noteId']') ensure we get all the notes, + // where $('#noteId') would return only one. + return function(i, el) { + var $note, $notes; + $note = $(el); + $notes = $note.closest('.discussion-notes'); + + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + if (gl.diffNoteApps[noteElId]) { + gl.diffNoteApps[noteElId].$destroy(); } + } - $note.remove(); + $note.remove(); - // check if this is the last note for this line - if ($notes.find('.note').length === 0) { - var notesTr = $notes.closest('tr'); + // check if this is the last note for this line + if ($notes.find('.note').length === 0) { + var notesTr = $notes.closest('tr'); - // "Discussions" tab - $notes.closest('.timeline-entry').remove(); + // "Discussions" tab + $notes.closest('.timeline-entry').remove(); - // The notes tr can contain multiple lists of notes, like on the parallel diff - if (notesTr.find('.discussion-notes').length > 1) { - $notes.remove(); - } else { - notesTr.remove(); - } + // The notes tr can contain multiple lists of notes, like on the parallel diff + if (notesTr.find('.discussion-notes').length > 1) { + $notes.remove(); + } else { + notesTr.remove(); } - }; - })(this)); - - Notes.checkMergeRequestStatus(); - return this.updateNotesCount(-1); - }; + } + }; + })(this)); + + Notes.checkMergeRequestStatus(); + return this.updateNotesCount(-1); + } + + /** + * Called in response to clicking the delete attachment link + * + * Removes the attachment wrapper view, including image tag if it exists + * Resets the note editing form + */ + removeAttachment() { + const $note = $(this).closest('.note'); + $note.find('.note-attachment').remove(); + $note.find('.note-body > .note-text').show(); + $note.find('.note-header').show(); + return $note.find('.current-note-edit-form').remove(); + } + + /** + * Called when clicking on the "reply" button for a diff line. + * + * Shows the note form below the notes. + */ + onReplyToDiscussionNote(e) { + this.replyToDiscussionNote(e.target); + } + + replyToDiscussionNote(target) { + var form, replyLink; + form = this.cleanForm(this.formClone.clone()); + replyLink = $(target).closest('.js-discussion-reply-button'); + // insert the form after the button + replyLink + .closest('.discussion-reply-holder') + .hide() + .after(form); + // show the form + return this.setupDiscussionNoteForm(replyLink, form); + } + + /** + * Shows the diff or discussion form and does some setup on it. + * + * Sets some hidden fields in the form. + * + * Note: dataHolder must have the "discussionId" and "lineCode" data attributes set. + */ + setupDiscussionNoteForm(dataHolder, form) { + // setup note target + const diffFileData = dataHolder.closest('.text-file'); + + var discussionID = dataHolder.data('discussionId'); + + if (discussionID) { + form.attr('data-discussion-id', discussionID); + form.find('#in_reply_to_discussion_id').val(discussionID); + } - /* - Called in response to clicking the delete attachment link + form.find('#note_project_id').val(dataHolder.data('discussionProjectId')); - Removes the attachment wrapper view, including image tag if it exists - Resets the note editing form - */ + form.attr('data-line-code', dataHolder.data('lineCode')); + form.find('#line_type').val(dataHolder.data('lineType')); - Notes.prototype.removeAttachment = function() { - const $note = $(this).closest('.note'); - $note.find('.note-attachment').remove(); - $note.find('.note-body > .note-text').show(); - $note.find('.note-header').show(); - return $note.find('.current-note-edit-form').remove(); - }; + form.find('#note_noteable_type').val(diffFileData.data('noteableType')); + form.find('#note_noteable_id').val(diffFileData.data('noteableId')); + form.find('#note_commit_id').val(diffFileData.data('commitId')); - /* - Called when clicking on the "reply" button for a diff line. + form.find('#note_type').val(dataHolder.data('noteType')); - Shows the note form below the notes. - */ + // LegacyDiffNote + form.find('#note_line_code').val(dataHolder.data('lineCode')); - Notes.prototype.onReplyToDiscussionNote = function(e) { - this.replyToDiscussionNote(e.target); - }; + // DiffNote + form.find('#note_position').val(dataHolder.attr('data-position')); - Notes.prototype.replyToDiscussionNote = function(target) { - var form, replyLink; - form = this.cleanForm(this.formClone.clone()); - replyLink = $(target).closest('.js-discussion-reply-button'); - // insert the form after the button - replyLink - .closest('.discussion-reply-holder') - .hide() - .after(form); - // show the form - return this.setupDiscussionNoteForm(replyLink, form); - }; + form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text')); + form.find('.js-note-target-close').remove(); + form.find('.js-note-new-discussion').remove(); + this.setupNoteForm(form); - /* - Shows the diff or discussion form and does some setup on it. + form + .removeClass('js-main-target-form') + .addClass('discussion-form js-discussion-note-form'); - Sets some hidden fields in the form. + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + var $commentBtn = form.find('comment-and-resolve-btn'); + $commentBtn.attr(':discussion-id', `'${discussionID}'`); - Note: dataHolder must have the "discussionId" and "lineCode" data attributes set. - */ + gl.diffNotesCompileComponents(); + } - Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) { - // setup note target - var discussionID = dataHolder.data('discussionId'); + form.find('.js-note-text').focus(); + form + .find('.js-comment-resolve-button') + .attr('data-discussion-id', discussionID); + } + + /** + * Called when clicking on the "add a comment" button on the side of a diff line. + * + * Inserts a temporary row for the form below the line. + * Sets up the form and shows it. + */ + onAddDiffNote(e) { + e.preventDefault(); + const link = e.currentTarget || e.target; + const $link = $(link); + const showReplyInput = !$link.hasClass('js-diff-comment-avatar'); + this.toggleDiffNote({ + target: $link, + lineType: link.dataset.lineType, + showReplyInput + }); + } + + toggleDiffNote({ + target, + lineType, + forceShow, + showReplyInput = false, + }) { + var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; + $link = $(target); + row = $link.closest('tr'); + const nextRow = row.next(); + let targetRow = row; + if (nextRow.is('.notes_holder')) { + targetRow = nextRow; + } - if (discussionID) { - form.attr('data-discussion-id', discussionID); - form.find('#in_reply_to_discussion_id').val(discussionID); + hasNotes = nextRow.is('.notes_holder'); + addForm = false; + let lineTypeSelector = ''; + rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; + // In parallel view, look inside the correct left/right pane + if (this.isParallelView()) { + lineTypeSelector = `.${lineType}`; + rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; + } + const notesContentSelector = `.notes_content${lineTypeSelector} .content`; + let notesContent = targetRow.find(notesContentSelector); + + if (hasNotes && showReplyInput) { + targetRow.show(); + notesContent = targetRow.find(notesContentSelector); + if (notesContent.length) { + notesContent.show(); + replyButton = notesContent.find('.js-discussion-reply-button:visible'); + if (replyButton.length) { + this.replyToDiscussionNote(replyButton[0]); + } else { + // In parallel view, the form may not be present in one of the panes + noteForm = notesContent.find('.js-discussion-note-form'); + if (noteForm.length === 0) { + addForm = true; + } + } } + } else if (showReplyInput) { + // add a notes row and insert the form + row.after(rowCssToAdd); + targetRow = row.next(); + notesContent = targetRow.find(notesContentSelector); + addForm = true; + } else { + const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible'); + const isForced = forceShow === true || forceShow === false; + const showNow = forceShow === true || (!isCurrentlyShown && !isForced); + + targetRow.toggle(showNow); + notesContent.toggle(showNow); + } - form.attr('data-line-code', dataHolder.data('lineCode')); - form.find('#line_type').val(dataHolder.data('lineType')); - - form.find('#note_noteable_type').val(dataHolder.data('noteableType')); - form.find('#note_noteable_id').val(dataHolder.data('noteableId')); - form.find('#note_commit_id').val(dataHolder.data('commitId')); - form.find('#note_type').val(dataHolder.data('noteType')); - - // LegacyDiffNote - form.find('#note_line_code').val(dataHolder.data('lineCode')); - - // DiffNote - form.find('#note_position').val(dataHolder.attr('data-position')); - - form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text')); - form.find('.js-note-target-close').remove(); - form.find('.js-note-new-discussion').remove(); - this.setupNoteForm(form); - - form - .removeClass('js-main-target-form') - .addClass('discussion-form js-discussion-note-form'); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - var $commentBtn = form.find('comment-and-resolve-btn'); - $commentBtn.attr(':discussion-id', `'${discussionID}'`); - - gl.diffNotesCompileComponents(); + if (addForm) { + newForm = this.cleanForm(this.formClone.clone()); + newForm.appendTo(notesContent); + // show the form + return this.setupDiscussionNoteForm($link, newForm); + } + } + + /** + * Called in response to "cancel" on a diff note form. + * + * Shows the reply button again. + * Removes the form and if necessary it's temporary row. + */ + removeDiscussionNoteForm(form) { + var glForm, row; + row = form.closest('tr'); + glForm = form.data('gl-form'); + glForm.destroy(); + form.find('.js-note-text').data('autosave').reset(); + // show the reply button (will only work for replies) + form + .prev('.discussion-reply-holder') + .show(); + if (row.is('.js-temp-notes-holder')) { + // remove temporary row for diff lines + return row.remove(); + } else { + // only remove the form + return form.remove(); + } + } + + cancelDiscussionForm(e) { + var form; + e.preventDefault(); + form = $(e.target).closest('.js-discussion-note-form'); + return this.removeDiscussionNoteForm(form); + } + + /** + * Called after an attachment file has been selected. + * + * Updates the file name for the selected attachment. + */ + updateFormAttachment() { + var filename, form; + form = $(this).closest('form'); + // get only the basename + filename = $(this).val().replace(/^.*[\\\/]/, ''); + return form.find('.js-attachment-filename').text(filename); + } + + /** + * Called when the tab visibility changes + */ + visibilityChange() { + return this.refresh(); + } + + updateTargetButtons(e) { + var closebtn, closetext, discardbtn, form, reopenbtn, reopentext, textarea; + textarea = $(e.target); + form = textarea.parents('form'); + reopenbtn = form.find('.js-note-target-reopen'); + closebtn = form.find('.js-note-target-close'); + discardbtn = form.find('.js-note-discard'); + + if (textarea.val().trim().length > 0) { + reopentext = reopenbtn.attr('data-alternative-text'); + closetext = closebtn.attr('data-alternative-text'); + if (reopenbtn.text() !== reopentext) { + reopenbtn.text(reopentext); } - - form.find('.js-note-text').focus(); - form - .find('.js-comment-resolve-button') - .attr('data-discussion-id', discussionID); - }; - - /* - Called when clicking on the "add a comment" button on the side of a diff line. - - Inserts a temporary row for the form below the line. - Sets up the form and shows it. - */ - - Notes.prototype.onAddDiffNote = function(e) { - e.preventDefault(); - const link = e.currentTarget || e.target; - const $link = $(link); - const showReplyInput = !$link.hasClass('js-diff-comment-avatar'); - this.toggleDiffNote({ - target: $link, - lineType: link.dataset.lineType, - showReplyInput - }); - }; - - Notes.prototype.toggleDiffNote = function({ - target, - lineType, - forceShow, - showReplyInput = false, - }) { - var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; - $link = $(target); - row = $link.closest('tr'); - const nextRow = row.next(); - let targetRow = row; - if (nextRow.is('.notes_holder')) { - targetRow = nextRow; + if (closebtn.text() !== closetext) { + closebtn.text(closetext); } - - hasNotes = nextRow.is('.notes_holder'); - addForm = false; - let lineTypeSelector = ''; - rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; - // In parallel view, look inside the correct left/right pane - if (this.isParallelView()) { - lineTypeSelector = `.${lineType}`; - rowCssToAdd = '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line old"></td><td class="notes_content parallel old"><div class="content"></div></td><td class="notes_line new"></td><td class="notes_content parallel new"><div class="content"></div></td></tr>'; + if (reopenbtn.is(':not(.btn-comment-and-reopen)')) { + reopenbtn.addClass('btn-comment-and-reopen'); } - const notesContentSelector = `.notes_content${lineTypeSelector} .content`; - let notesContent = targetRow.find(notesContentSelector); - - if (hasNotes && showReplyInput) { - targetRow.show(); - notesContent = targetRow.find(notesContentSelector); - if (notesContent.length) { - notesContent.show(); - replyButton = notesContent.find('.js-discussion-reply-button:visible'); - if (replyButton.length) { - this.replyToDiscussionNote(replyButton[0]); - } else { - // In parallel view, the form may not be present in one of the panes - noteForm = notesContent.find('.js-discussion-note-form'); - if (noteForm.length === 0) { - addForm = true; - } - } - } - } else if (showReplyInput) { - // add a notes row and insert the form - row.after(rowCssToAdd); - targetRow = row.next(); - notesContent = targetRow.find(notesContentSelector); - addForm = true; - } else { - const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible'); - const isForced = forceShow === true || forceShow === false; - const showNow = forceShow === true || (!isCurrentlyShown && !isForced); - - targetRow.toggle(showNow); - notesContent.toggle(showNow); + if (closebtn.is(':not(.btn-comment-and-close)')) { + closebtn.addClass('btn-comment-and-close'); } - - if (addForm) { - newForm = this.cleanForm(this.formClone.clone()); - newForm.appendTo(notesContent); - // show the form - return this.setupDiscussionNoteForm($link, newForm); + if (discardbtn.is(':hidden')) { + return discardbtn.show(); } - }; - - /* - Called in response to "cancel" on a diff note form. - - Shows the reply button again. - Removes the form and if necessary it's temporary row. - */ - - Notes.prototype.removeDiscussionNoteForm = function(form) { - var glForm, row; - row = form.closest('tr'); - glForm = form.data('gl-form'); - glForm.destroy(); - form.find('.js-note-text').data('autosave').reset(); - // show the reply button (will only work for replies) - form - .prev('.discussion-reply-holder') - .show(); - if (row.is('.js-temp-notes-holder')) { - // remove temporary row for diff lines - return row.remove(); - } else { - // only remove the form - return form.remove(); + } else { + reopentext = reopenbtn.data('original-text'); + closetext = closebtn.data('original-text'); + if (reopenbtn.text() !== reopentext) { + reopenbtn.text(reopentext); } - }; - - Notes.prototype.cancelDiscussionForm = function(e) { - var form; - e.preventDefault(); - form = $(e.target).closest('.js-discussion-note-form'); - return this.removeDiscussionNoteForm(form); - }; - - /* - Called after an attachment file has been selected. - - Updates the file name for the selected attachment. - */ - - Notes.prototype.updateFormAttachment = function() { - var filename, form; - form = $(this).closest('form'); - // get only the basename - filename = $(this).val().replace(/^.*[\\\/]/, ''); - return form.find('.js-attachment-filename').text(filename); - }; - - /* - Called when the tab visibility changes - */ - - Notes.prototype.visibilityChange = function() { - return this.refresh(); - }; - - Notes.prototype.updateTargetButtons = function(e) { - var closebtn, closetext, discardbtn, form, reopenbtn, reopentext, textarea; - textarea = $(e.target); - form = textarea.parents('form'); - reopenbtn = form.find('.js-note-target-reopen'); - closebtn = form.find('.js-note-target-close'); - discardbtn = form.find('.js-note-discard'); - - if (textarea.val().trim().length > 0) { - reopentext = reopenbtn.attr('data-alternative-text'); - closetext = closebtn.attr('data-alternative-text'); - if (reopenbtn.text() !== reopentext) { - reopenbtn.text(reopentext); - } - if (closebtn.text() !== closetext) { - closebtn.text(closetext); - } - if (reopenbtn.is(':not(.btn-comment-and-reopen)')) { - reopenbtn.addClass('btn-comment-and-reopen'); - } - if (closebtn.is(':not(.btn-comment-and-close)')) { - closebtn.addClass('btn-comment-and-close'); - } - if (discardbtn.is(':hidden')) { - return discardbtn.show(); - } - } else { - reopentext = reopenbtn.data('original-text'); - closetext = closebtn.data('original-text'); - if (reopenbtn.text() !== reopentext) { - reopenbtn.text(reopentext); - } - if (closebtn.text() !== closetext) { - closebtn.text(closetext); - } - if (reopenbtn.is('.btn-comment-and-reopen')) { - reopenbtn.removeClass('btn-comment-and-reopen'); - } - if (closebtn.is('.btn-comment-and-close')) { - closebtn.removeClass('btn-comment-and-close'); - } - if (discardbtn.is(':visible')) { - return discardbtn.hide(); - } + if (closebtn.text() !== closetext) { + closebtn.text(closetext); } - }; - - Notes.prototype.putEditFormInPlace = function($el) { - var $editForm = $(this.getEditFormSelector($el)); - var $note = $el.closest('.note'); - - $editForm.insertAfter($note.find('.note-text')); - - var $originalContentEl = $note.find('.original-note-content'); - var originalContent = $originalContentEl.text().trim(); - var postUrl = $originalContentEl.data('post-url'); - var targetId = $originalContentEl.data('target-id'); - var targetType = $originalContentEl.data('target-type'); - - new gl.GLForm($editForm.find('form'), this.enableGFM); - - $editForm.find('form') - .attr('action', postUrl) - .attr('data-remote', 'true'); - $editForm.find('.js-form-target-id').val(targetId); - $editForm.find('.js-form-target-type').val(targetType); - $editForm.find('.js-note-text').focus().val(originalContent); - $editForm.find('.js-md-write-button').trigger('click'); - $editForm.find('.referenced-users').hide(); - }; - - Notes.prototype.putConflictEditWarningInPlace = function(noteEntity, $note) { - if ($note.find('.js-conflict-edit-warning').length === 0) { - const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger"> - This comment has changed since you started editing, please review the - <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer"> - updated comment - </a> - to ensure information is not lost - </div>`); - $alert.insertAfter($note.find('.note-text')); + if (reopenbtn.is('.btn-comment-and-reopen')) { + reopenbtn.removeClass('btn-comment-and-reopen'); } - }; - - Notes.prototype.updateNotesCount = function(updateCount) { - return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); - }; - - Notes.prototype.toggleCommitList = function(e) { - const $element = $(e.currentTarget); - const $closestSystemCommitList = $element.siblings('.system-note-commit-list'); - - $element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up'); - $closestSystemCommitList.toggleClass('hide-shade'); - }; - - /** - Scans system notes with `ul` elements in system note body - then collapse long commit list pushed by user to make it less - intrusive. - */ - Notes.prototype.collapseLongCommitList = function() { - const systemNotes = $('#notes-list').find('li.system-note').has('ul'); - - $.each(systemNotes, function(index, systemNote) { - const $systemNote = $(systemNote); - const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', ''); - - $systemNote.find('.note-header .system-note-message').html(headerMessage); - - if ($systemNote.find('li').length > MAX_VISIBLE_COMMIT_LIST_COUNT) { - $systemNote.find('.note-text').addClass('system-note-commit-list'); - $systemNote.find('.system-note-commit-list-toggler').show(); - } else { - $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade'); - } - }); - }; - - Notes.prototype.addFlash = function(...flashParams) { - this.flashInstance = new Flash(...flashParams); - }; - - Notes.prototype.clearFlash = function() { - if (this.flashInstance && this.flashInstance.flashContainer) { - this.flashInstance.flashContainer.hide(); - this.flashInstance = null; + if (closebtn.is('.btn-comment-and-close')) { + closebtn.removeClass('btn-comment-and-close'); } - }; - - Notes.prototype.cleanForm = function($form) { - // Remove JS classes that are not needed here - $form - .find('.js-comment-type-dropdown') - .removeClass('btn-group'); - - // Remove dropdown - $form - .find('.dropdown-menu') - .remove(); - - return $form; - }; - - /** - * Check if note does not exists on page - */ - Notes.isNewNote = function(noteEntity, noteIds) { - return $.inArray(noteEntity.id, noteIds) === -1; - }; - - /** - * Check if $note already contains the `noteEntity` content - */ - Notes.isUpdatedNote = function(noteEntity, $note) { - // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way - const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim()); - const currentNoteText = normalizeNewlines( - $note.find('.original-note-content').first().text().trim() - ); - return sanitizedNoteEntityText !== currentNoteText; - }; - - Notes.checkMergeRequestStatus = function() { - if (gl.utils.getPagePath(1) === 'merge_requests') { - gl.mrWidget.checkStatus(); + if (discardbtn.is(':visible')) { + return discardbtn.hide(); } - }; + } + } + + putEditFormInPlace($el) { + var $editForm = $(this.getEditFormSelector($el)); + var $note = $el.closest('.note'); + + $editForm.insertAfter($note.find('.note-text')); + + var $originalContentEl = $note.find('.original-note-content'); + var originalContent = $originalContentEl.text().trim(); + var postUrl = $originalContentEl.data('post-url'); + var targetId = $originalContentEl.data('target-id'); + var targetType = $originalContentEl.data('target-type'); + + new gl.GLForm($editForm.find('form'), this.enableGFM); + + $editForm.find('form') + .attr('action', postUrl) + .attr('data-remote', 'true'); + $editForm.find('.js-form-target-id').val(targetId); + $editForm.find('.js-form-target-type').val(targetType); + $editForm.find('.js-note-text').focus().val(originalContent); + $editForm.find('.js-md-write-button').trigger('click'); + $editForm.find('.referenced-users').hide(); + } + + putConflictEditWarningInPlace(noteEntity, $note) { + if ($note.find('.js-conflict-edit-warning').length === 0) { + const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger"> + This comment has changed since you started editing, please review the + <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer"> + updated comment + </a> + to ensure information is not lost + </div>`); + $alert.insertAfter($note.find('.note-text')); + } + } - Notes.animateAppendNote = function(noteHtml, $notesList) { - const $note = $(noteHtml); + updateNotesCount(updateCount) { + return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); + } - $note.addClass('fade-in-full').renderGFM(); - $notesList.append($note); - return $note; - }; + toggleCommitList(e) { + const $element = $(e.currentTarget); + const $closestSystemCommitList = $element.siblings('.system-note-commit-list'); - Notes.animateUpdateNote = function(noteHtml, $note) { - const $updatedNote = $(noteHtml); + $element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up'); + $closestSystemCommitList.toggleClass('hide-shade'); + } - $updatedNote.addClass('fade-in').renderGFM(); - $note.replaceWith($updatedNote); - return $updatedNote; - }; + /** + * Scans system notes with `ul` elements in system note body + * then collapse long commit list pushed by user to make it less + * intrusive. + */ + collapseLongCommitList() { + const systemNotes = $('#notes-list').find('li.system-note').has('ul'); - /** - * Get data from Form attributes to use for saving/submitting comment. - */ - Notes.prototype.getFormData = function($form) { - return { - formData: $form.serialize(), - formContent: _.escape($form.find('.js-note-text').val()), - formAction: $form.attr('action'), - }; - }; + $.each(systemNotes, function(index, systemNote) { + const $systemNote = $(systemNote); + const headerMessage = $systemNote.find('.note-text').find('p:first').text().replace(':', ''); - /** - * Identify if comment has any quick actions - */ - Notes.prototype.hasQuickActions = function(formContent) { - return REGEX_QUICK_ACTIONS.test(formContent); - }; - - /** - * Remove quick actions and leave comment with pure message - */ - Notes.prototype.stripQuickActions = function(formContent) { - return formContent.replace(REGEX_QUICK_ACTIONS, '').trim(); - }; + $systemNote.find('.note-header .system-note-message').html(headerMessage); - /** - * Gets appropriate description from quick actions found in provided `formContent` - */ - Notes.prototype.getQuickActionDescription = function (formContent, availableQuickActions = []) { - let tempFormContent; + if ($systemNote.find('li').length > MAX_VISIBLE_COMMIT_LIST_COUNT) { + $systemNote.find('.note-text').addClass('system-note-commit-list'); + $systemNote.find('.system-note-commit-list-toggler').show(); + } else { + $systemNote.find('.note-text').addClass('system-note-commit-list hide-shade'); + } + }); + } - // Identify executed quick actions from `formContent` - const executedCommands = availableQuickActions.filter((command, index) => { - const commandRegex = new RegExp(`/${command.name}`); - return commandRegex.test(formContent); - }); + addFlash(...flashParams) { + this.flashInstance = new Flash(...flashParams); + } - if (executedCommands && executedCommands.length) { - if (executedCommands.length > 1) { - tempFormContent = 'Applying multiple commands'; - } else { - const commandDescription = executedCommands[0].description.toLowerCase(); - tempFormContent = `Applying command to ${commandDescription}`; - } + clearFlash() { + if (this.flashInstance && this.flashInstance.flashContainer) { + this.flashInstance.flashContainer.hide(); + this.flashInstance = null; + } + } + + cleanForm($form) { + // Remove JS classes that are not needed here + $form + .find('.js-comment-type-dropdown') + .removeClass('btn-group'); + + // Remove dropdown + $form + .find('.dropdown-menu') + .remove(); + + return $form; + } + + /** + * Check if note does not exists on page + */ + static isNewNote(noteEntity, noteIds) { + return $.inArray(noteEntity.id, noteIds) === -1; + } + + /** + * Check if $note already contains the `noteEntity` content + */ + static isUpdatedNote(noteEntity, $note) { + // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way + const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim()); + const currentNoteText = normalizeNewlines( + $note.find('.original-note-content').first().text().trim() + ); + return sanitizedNoteEntityText !== currentNoteText; + } + + static checkMergeRequestStatus() { + if (gl.utils.getPagePath(1) === 'merge_requests') { + gl.mrWidget.checkStatus(); + } + } + + static animateAppendNote(noteHtml, $notesList) { + const $note = $(noteHtml); + + $note.addClass('fade-in-full').renderGFM(); + $notesList.append($note); + return $note; + } + + static animateUpdateNote(noteHtml, $note) { + const $updatedNote = $(noteHtml); + + $updatedNote.addClass('fade-in').renderGFM(); + $note.replaceWith($updatedNote); + return $updatedNote; + } + + /** + * Get data from Form attributes to use for saving/submitting comment. + */ + getFormData($form) { + return { + formData: $form.serialize(), + formContent: _.escape($form.find('.js-note-text').val()), + formAction: $form.attr('action'), + }; + } + + /** + * Identify if comment has any quick actions + */ + hasQuickActions(formContent) { + return REGEX_QUICK_ACTIONS.test(formContent); + } + + /** + * Remove quick actions and leave comment with pure message + */ + stripQuickActions(formContent) { + return formContent.replace(REGEX_QUICK_ACTIONS, '').trim(); + } + + /** + * Gets appropriate description from quick actions found in provided `formContent` + */ + getQuickActionDescription(formContent, availableQuickActions = []) { + let tempFormContent; + + // Identify executed quick actions from `formContent` + const executedCommands = availableQuickActions.filter((command, index) => { + const commandRegex = new RegExp(`/${command.name}`); + return commandRegex.test(formContent); + }); + + if (executedCommands && executedCommands.length) { + if (executedCommands.length > 1) { + tempFormContent = 'Applying multiple commands'; } else { - tempFormContent = 'Applying command'; + const commandDescription = executedCommands[0].description.toLowerCase(); + tempFormContent = `Applying command to ${commandDescription}`; } + } else { + tempFormContent = 'Applying command'; + } - return tempFormContent; - }; - - /** - * Create placeholder note DOM element populated with comment body - * that we will show while comment is being posted. - * Once comment is _actually_ posted on server, we will have final element - * in response that we will show in place of this temporary element. - */ - Notes.prototype.createPlaceholderNote = function ({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) { - const discussionClass = isDiscussionNote ? 'discussion' : ''; - const $tempNote = $( - `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> - <div class="timeline-entry-inner"> - <div class="timeline-icon"> - <a href="/${currentUsername}"> - <img class="avatar s40" src="${currentUserAvatar}"> - </a> - </div> - <div class="timeline-content ${discussionClass}"> - <div class="note-header"> - <div class="note-header-info"> - <a href="/${currentUsername}"> - <span class="hidden-xs">${currentUserFullname}</span> - <span class="note-headline-light">@${currentUsername}</span> - </a> - </div> + return tempFormContent; + } + + /** + * Create placeholder note DOM element populated with comment body + * that we will show while comment is being posted. + * Once comment is _actually_ posted on server, we will have final element + * in response that we will show in place of this temporary element. + */ + createPlaceholderNote({ formContent, uniqueId, isDiscussionNote, currentUsername, currentUserFullname, currentUserAvatar }) { + const discussionClass = isDiscussionNote ? 'discussion' : ''; + const $tempNote = $( + `<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry"> + <div class="timeline-entry-inner"> + <div class="timeline-icon"> + <a href="/${currentUsername}"> + <img class="avatar s40" src="${currentUserAvatar}" /> + </a> + </div> + <div class="timeline-content ${discussionClass}"> + <div class="note-header"> + <div class="note-header-info"> + <a href="/${currentUsername}"> + <span class="hidden-xs">${currentUserFullname}</span> + <span class="note-headline-light">@${currentUsername}</span> + </a> + </div> + </div> + <div class="note-body"> + <div class="note-text"> + <p>${formContent}</p> </div> - <div class="note-body"> - <div class="note-text"> - <p>${formContent}</p> - </div> - </div> - </div> - </div> - </li>` - ); - - return $tempNote; - }; - - /** - * Create Placeholder System Note DOM element populated with quick action description - */ - Notes.prototype.createPlaceholderSystemNote = function ({ formContent, uniqueId }) { - const $tempNote = $( - `<li id="${uniqueId}" class="note system-note timeline-entry being-posted fade-in-half"> - <div class="timeline-entry-inner"> - <div class="timeline-content"> - <i>${formContent}</i> - </div> + </div> + </div> + </div> + </li>` + ); + + return $tempNote; + } + + /** + * Create Placeholder System Note DOM element populated with quick action description + */ + createPlaceholderSystemNote({ formContent, uniqueId }) { + const $tempNote = $( + `<li id="${uniqueId}" class="note system-note timeline-entry being-posted fade-in-half"> + <div class="timeline-entry-inner"> + <div class="timeline-content"> + <i>${formContent}</i> </div> - </li>` - ); + </div> + </li>` + ); + + return $tempNote; + } + + /** + * This method does following tasks step-by-step whenever a new comment + * is submitted by user (both main thread comments as well as discussion comments). + * + * 1) Get Form metadata + * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve + * 3) Build temporary placeholder element (using `createPlaceholderNote`) + * 4) Show placeholder note on UI + * 5) Perform network request to submit the note using `gl.utils.ajaxPost` + * a) If request is successfully completed + * 1. Remove placeholder element + * 2. Show submitted Note element + * 3. Perform post-submit errands + * a. Mark discussion as resolved if comment submission was for resolve. + * b. Reset comment form to original state. + * b) If request failed + * 1. Remove placeholder element + * 2. Show error Flash message about failure + */ + postComment(e) { + e.preventDefault(); + + // Get Form metadata + const $submitBtn = $(e.target); + let $form = $submitBtn.parents('form'); + const $closeBtn = $form.find('.js-note-target-close'); + const isDiscussionNote = $submitBtn.parent().find('li.droplab-item-selected').attr('id') === 'discussion'; + const isMainForm = $form.hasClass('js-main-target-form'); + const isDiscussionForm = $form.hasClass('js-discussion-note-form'); + const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); + const { formData, formContent, formAction } = this.getFormData($form); + let noteUniqueId; + let systemNoteUniqueId; + let hasQuickActions = false; + let $notesContainer; + let tempFormContent; + + // Get reference to notes container based on type of comment + if (isDiscussionForm) { + $notesContainer = $form.parent('.discussion-notes').find('.notes'); + } else if (isMainForm) { + $notesContainer = $('ul.main-notes-list'); + } - return $tempNote; - }; + // If comment is to resolve discussion, disable submit buttons while + // comment posting is finished. + if (isDiscussionResolve) { + $submitBtn.disable(); + $form.find('.js-comment-submit-button').disable(); + } - /** - * This method does following tasks step-by-step whenever a new comment - * is submitted by user (both main thread comments as well as discussion comments). - * - * 1) Get Form metadata - * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve - * 3) Build temporary placeholder element (using `createPlaceholderNote`) - * 4) Show placeholder note on UI - * 5) Perform network request to submit the note using `gl.utils.ajaxPost` - * a) If request is successfully completed - * 1. Remove placeholder element - * 2. Show submitted Note element - * 3. Perform post-submit errands - * a. Mark discussion as resolved if comment submission was for resolve. - * b. Reset comment form to original state. - * b) If request failed - * 1. Remove placeholder element - * 2. Show error Flash message about failure - */ - Notes.prototype.postComment = function(e) { - e.preventDefault(); - - // Get Form metadata - const $submitBtn = $(e.target); - let $form = $submitBtn.parents('form'); - const $closeBtn = $form.find('.js-note-target-close'); - const isDiscussionNote = $submitBtn.parent().find('li.droplab-item-selected').attr('id') === 'discussion'; - const isMainForm = $form.hasClass('js-main-target-form'); - const isDiscussionForm = $form.hasClass('js-discussion-note-form'); - const isDiscussionResolve = $submitBtn.hasClass('js-comment-resolve-button'); - const { formData, formContent, formAction } = this.getFormData($form); - let noteUniqueId; - let systemNoteUniqueId; - let hasQuickActions = false; - let $notesContainer; - let tempFormContent; - - // Get reference to notes container based on type of comment - if (isDiscussionForm) { - $notesContainer = $form.parent('.discussion-notes').find('.notes'); - } else if (isMainForm) { - $notesContainer = $('ul.main-notes-list'); - } + tempFormContent = formContent; + if (this.hasQuickActions(formContent)) { + tempFormContent = this.stripQuickActions(formContent); + hasQuickActions = true; + } - // If comment is to resolve discussion, disable submit buttons while - // comment posting is finished. - if (isDiscussionResolve) { - $submitBtn.disable(); - $form.find('.js-comment-submit-button').disable(); - } + // Show placeholder note + if (tempFormContent) { + noteUniqueId = _.uniqueId('tempNote_'); + $notesContainer.append(this.createPlaceholderNote({ + formContent: tempFormContent, + uniqueId: noteUniqueId, + isDiscussionNote, + currentUsername: gon.current_username, + currentUserFullname: gon.current_user_fullname, + currentUserAvatar: gon.current_user_avatar_url, + })); + } - tempFormContent = formContent; - if (this.hasQuickActions(formContent)) { - tempFormContent = this.stripQuickActions(formContent); - hasQuickActions = true; - } + // Show placeholder system note + if (hasQuickActions) { + systemNoteUniqueId = _.uniqueId('tempSystemNote_'); + $notesContainer.append(this.createPlaceholderSystemNote({ + formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), + uniqueId: systemNoteUniqueId, + })); + } - // Show placeholder note - if (tempFormContent) { - noteUniqueId = _.uniqueId('tempNote_'); - $notesContainer.append(this.createPlaceholderNote({ - formContent: tempFormContent, - uniqueId: noteUniqueId, - isDiscussionNote, - currentUsername: gon.current_username, - currentUserFullname: gon.current_user_fullname, - currentUserAvatar: gon.current_user_avatar_url, - })); + // Clear the form textarea + if ($notesContainer.length) { + if (isMainForm) { + this.resetMainTargetForm(e); + } else if (isDiscussionForm) { + this.removeDiscussionNoteForm($form); } + } - // Show placeholder system note - if (hasQuickActions) { - systemNoteUniqueId = _.uniqueId('tempSystemNote_'); - $notesContainer.append(this.createPlaceholderSystemNote({ - formContent: this.getQuickActionDescription(formContent, AjaxCache.get(gl.GfmAutoComplete.dataSources.commands)), - uniqueId: systemNoteUniqueId, - })); - } + /* eslint-disable promise/catch-or-return */ + // Make request to submit comment on server + gl.utils.ajaxPost(formAction, formData) + .then((note) => { + // Submission successful! remove placeholder + $notesContainer.find(`#${noteUniqueId}`).remove(); - // Clear the form textarea - if ($notesContainer.length) { - if (isMainForm) { - this.resetMainTargetForm(e); - } else if (isDiscussionForm) { - this.removeDiscussionNoteForm($form); + // Reset cached commands list when command is applied + if (hasQuickActions) { + $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); } - } - - /* eslint-disable promise/catch-or-return */ - // Make request to submit comment on server - gl.utils.ajaxPost(formAction, formData) - .then((note) => { - // Submission successful! remove placeholder - $notesContainer.find(`#${noteUniqueId}`).remove(); - // Reset cached commands list when command is applied - if (hasQuickActions) { - $form.find('textarea.js-note-text').trigger('clear-commands-cache.atwho'); - } - - // Clear previous form errors - this.clearFlashWrapper(); - - // Check if this was discussion comment - if (isDiscussionForm) { - // Remove flash-container - $notesContainer.find('.flash-container').remove(); + // Clear previous form errors + this.clearFlashWrapper(); - // If comment intends to resolve discussion, do the same. - if (isDiscussionResolve) { - $form - .attr('data-discussion-id', $submitBtn.data('discussion-id')) - .attr('data-resolve-all', 'true') - .attr('data-project-path', $submitBtn.data('project-path')); - } - - // Show final note element on UI - this.addDiscussionNote($form, note, $notesContainer.length === 0); + // Check if this was discussion comment + if (isDiscussionForm) { + // Remove flash-container + $notesContainer.find('.flash-container').remove(); - // append flash-container to the Notes list - if ($notesContainer.length) { - $notesContainer.append('<div class="flash-container" style="display: none;"></div>'); - } - } else if (isMainForm) { // Check if this was main thread comment - // Show final note element on UI and perform form and action buttons cleanup - this.addNote($form, note); - this.reenableTargetFormSubmitButton(e); + // If comment intends to resolve discussion, do the same. + if (isDiscussionResolve) { + $form + .attr('data-discussion-id', $submitBtn.data('discussion-id')) + .attr('data-resolve-all', 'true') + .attr('data-project-path', $submitBtn.data('project-path')); } - if (note.commands_changes) { - this.handleQuickActions(note); + // Show final note element on UI + this.addDiscussionNote($form, note, $notesContainer.length === 0); + + // append flash-container to the Notes list + if ($notesContainer.length) { + $notesContainer.append('<div class="flash-container" style="display: none;"></div>'); } + } else if (isMainForm) { // Check if this was main thread comment + // Show final note element on UI and perform form and action buttons cleanup + this.addNote($form, note); + this.reenableTargetFormSubmitButton(e); + } - $form.trigger('ajax:success', [note]); - }).fail(() => { - // Submission failed, remove placeholder note and show Flash error message - $notesContainer.find(`#${noteUniqueId}`).remove(); + if (note.commands_changes) { + this.handleQuickActions(note); + } - if (hasQuickActions) { - $notesContainer.find(`#${systemNoteUniqueId}`).remove(); - } + $form.trigger('ajax:success', [note]); + }).fail(() => { + // Submission failed, remove placeholder note and show Flash error message + $notesContainer.find(`#${noteUniqueId}`).remove(); - // Show form again on UI on failure - if (isDiscussionForm && $notesContainer.length) { - const replyButton = $notesContainer.parent().find('.js-discussion-reply-button'); - this.replyToDiscussionNote(replyButton[0]); - $form = $notesContainer.parent().find('form'); - } + if (hasQuickActions) { + $notesContainer.find(`#${systemNoteUniqueId}`).remove(); + } - $form.find('.js-note-text').val(formContent); - this.reenableTargetFormSubmitButton(e); - this.addNoteError($form); - }); + // Show form again on UI on failure + if (isDiscussionForm && $notesContainer.length) { + const replyButton = $notesContainer.parent().find('.js-discussion-reply-button'); + this.replyToDiscussionNote(replyButton[0]); + $form = $notesContainer.parent().find('form'); + } - return $closeBtn.text($closeBtn.data('original-text')); - }; + $form.find('.js-note-text').val(formContent); + this.reenableTargetFormSubmitButton(e); + this.addNoteError($form); + }); - /** - * This method does following tasks step-by-step whenever an existing comment - * is updated by user (both main thread comments as well as discussion comments). - * - * 1) Get Form metadata - * 2) Update note element with new content - * 3) Perform network request to submit the updated note using `gl.utils.ajaxPost` - * a) If request is successfully completed - * 1. Show submitted Note element - * b) If request failed - * 1. Revert Note element to original content - * 2. Show error Flash message about failure - */ - Notes.prototype.updateComment = function(e) { - e.preventDefault(); - - // Get Form metadata - const $submitBtn = $(e.target); - const $form = $submitBtn.parents('form'); - const $closeBtn = $form.find('.js-note-target-close'); - const $editingNote = $form.parents('.note.is-editing'); - const $noteBody = $editingNote.find('.js-task-list-container'); - const $noteBodyText = $noteBody.find('.note-text'); - const { formData, formContent, formAction } = this.getFormData($form); - - // Cache original comment content - const cachedNoteBodyText = $noteBodyText.html(); - - // Show updated comment content temporarily - $noteBodyText.html(_.escape(formContent)); - $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half'); - $editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>'); - - /* eslint-disable promise/catch-or-return */ - // Make request to update comment on server - gl.utils.ajaxPost(formAction, formData) - .then((note) => { - // Submission successful! render final note element - this.updateNote(note, $editingNote); - }) - .fail(() => { - // Submission failed, revert back to original note - $noteBodyText.html(_.escape(cachedNoteBodyText)); - $editingNote.removeClass('being-posted fade-in'); - $editingNote.find('.fa.fa-spinner').remove(); - - // Show Flash message about failure - this.updateNoteError(); - }); + return $closeBtn.text($closeBtn.data('original-text')); + } + + /** + * This method does following tasks step-by-step whenever an existing comment + * is updated by user (both main thread comments as well as discussion comments). + * + * 1) Get Form metadata + * 2) Update note element with new content + * 3) Perform network request to submit the updated note using `gl.utils.ajaxPost` + * a) If request is successfully completed + * 1. Show submitted Note element + * b) If request failed + * 1. Revert Note element to original content + * 2. Show error Flash message about failure + */ + updateComment(e) { + e.preventDefault(); + + // Get Form metadata + const $submitBtn = $(e.target); + const $form = $submitBtn.parents('form'); + const $closeBtn = $form.find('.js-note-target-close'); + const $editingNote = $form.parents('.note.is-editing'); + const $noteBody = $editingNote.find('.js-task-list-container'); + const $noteBodyText = $noteBody.find('.note-text'); + const { formData, formContent, formAction } = this.getFormData($form); + + // Cache original comment content + const cachedNoteBodyText = $noteBodyText.html(); + + // Show updated comment content temporarily + $noteBodyText.html(formContent); + $editingNote.removeClass('is-editing fade-in-full').addClass('being-posted fade-in-half'); + $editingNote.find('.note-headline-meta a').html('<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>'); + + /* eslint-disable promise/catch-or-return */ + // Make request to update comment on server + gl.utils.ajaxPost(formAction, formData) + .then((note) => { + // Submission successful! render final note element + this.updateNote(note, $editingNote); + }) + .fail(() => { + // Submission failed, revert back to original note + $noteBodyText.html(_.escape(cachedNoteBodyText)); + $editingNote.removeClass('being-posted fade-in'); + $editingNote.find('.fa.fa-spinner').remove(); + + // Show Flash message about failure + this.updateNoteError(); + }); - return $closeBtn.text($closeBtn.data('original-text')); - }; + return $closeBtn.text($closeBtn.data('original-text')); + } +} - return Notes; - })(); -}).call(window); +window.Notes = Notes; diff --git a/app/assets/javascripts/oauth_remember_me.js b/app/assets/javascripts/oauth_remember_me.js new file mode 100644 index 00000000000..ffc2dd6bbca --- /dev/null +++ b/app/assets/javascripts/oauth_remember_me.js @@ -0,0 +1,32 @@ +/** + * OAuth-based login buttons have a separate "remember me" checkbox. + * + * Toggling this checkbox adds/removes a `remember_me` parameter to the + * login buttons' href, which is passed on to the omniauth callback. + **/ + +export default class OAuthRememberMe { + constructor(opts = {}) { + this.container = opts.container || ''; + this.loginLinkSelector = '.oauth-login'; + } + + bindEvents() { + $('#remember_me', this.container).on('click', this.toggleRememberMe); + } + + // eslint-disable-next-line class-methods-use-this + toggleRememberMe(event) { + const rememberMe = $(event.target).is(':checked'); + + $('.oauth-login', this.container).each((i, element) => { + const href = $(element).attr('href'); + + if (rememberMe) { + $(element).attr('href', `${href}?remember_me=1`); + } else { + $(element).attr('href', href.replace('?remember_me=1', '')); + } + }); + } +} diff --git a/app/assets/javascripts/peek.js b/app/assets/javascripts/peek.js deleted file mode 100644 index de1a99fa3bd..00000000000 --- a/app/assets/javascripts/peek.js +++ /dev/null @@ -1,16 +0,0 @@ -import 'vendor/peek'; -import 'vendor/peek.performance_bar'; - -$(document).on('click', '#peek-show-queries', (e) => { - e.preventDefault(); - $('.peek-rblineprof-modal').hide(); - const $modal = $('#modal-peek-pg-queries'); - if ($modal.length) { - $modal.modal('toggle'); - } -}); - -$(document).on('click', '.js-lineprof-file', (e) => { - e.preventDefault(); - $(e.target).parents('.peek-rblineprof-file').find('.data').toggle(); -}); diff --git a/app/assets/javascripts/performance_bar.js b/app/assets/javascripts/performance_bar.js new file mode 100644 index 00000000000..9bbdf7f513c --- /dev/null +++ b/app/assets/javascripts/performance_bar.js @@ -0,0 +1,62 @@ +import 'vendor/peek'; +import 'vendor/peek.performance_bar'; + +export default class PerformanceBar { + constructor(opts) { + if (!PerformanceBar.singleton) { + this.init(opts); + PerformanceBar.singleton = this; + } + return PerformanceBar.singleton; + } + + init(opts) { + const $container = $(opts.container); + this.$sqlProfileLink = $container.find('.js-toggle-modal-peek-sql'); + this.$sqlProfileModal = $container.find('#modal-peek-pg-queries'); + this.$lineProfileLink = $container.find('.js-toggle-modal-peek-line-profile'); + this.$lineProfileModal = $('#modal-peek-line-profile'); + this.initEventListeners(); + this.showModalOnLoad(); + } + + initEventListeners() { + this.$sqlProfileLink.on('click', () => this.handleSQLProfileLink()); + this.$lineProfileLink.on('click', e => this.handleLineProfileLink(e)); + $(document).on('click', '.js-lineprof-file', PerformanceBar.toggleLineProfileFile); + } + + showModalOnLoad() { + // When a lineprofiler query-string param is present, we show the line + // profiler modal upon page load + if (/lineprofiler/.test(window.location.search)) { + PerformanceBar.toggleModal(this.$lineProfileModal); + } + } + + handleSQLProfileLink() { + PerformanceBar.toggleModal(this.$sqlProfileModal); + } + + handleLineProfileLink(e) { + const lineProfilerParameter = gl.utils.getParameterValues('lineprofiler'); + const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`); + const shouldToggleModal = lineProfilerParameter.length > 0 && + lineProfilerParameterRegex.test(e.currentTarget.href); + + if (shouldToggleModal) { + e.preventDefault(); + PerformanceBar.toggleModal(this.$lineProfileModal); + } + } + + static toggleModal($modal) { + if ($modal.length) { + $modal.modal('toggle'); + } + } + + static toggleLineProfileFile(e) { + $(e.currentTarget).parents('.peek-rblineprof-file').find('.data').toggle(); + } +} diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js deleted file mode 100644 index 901adbe9fce..00000000000 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js +++ /dev/null @@ -1,148 +0,0 @@ -import Vue from 'vue'; -import Translate from '../../vue_shared/translate'; - -Vue.use(Translate); - -const inputNameAttribute = 'schedule[cron]'; - -export default { - props: { - initialCronInterval: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - inputNameAttribute, - cronInterval: this.initialCronInterval, - cronIntervalPresets: { - everyDay: '0 4 * * *', - everyWeek: '0 4 * * 0', - everyMonth: '0 4 1 * *', - }, - cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', - customInputEnabled: false, - }; - }, - computed: { - intervalIsPreset() { - return _.contains(this.cronIntervalPresets, this.cronInterval); - }, - // The text input is editable when there's a custom interval, or when it's - // a preset interval and the user clicks the 'custom' radio button - isEditable() { - return !!(this.customInputEnabled || !this.intervalIsPreset); - }, - }, - methods: { - toggleCustomInput(shouldEnable) { - this.customInputEnabled = shouldEnable; - - if (shouldEnable) { - // We need to change the value so other radios don't remain selected - // because the model (cronInterval) hasn't changed. The server trims it. - this.cronInterval = `${this.cronInterval} `; - } - }, - }, - created() { - if (this.intervalIsPreset) { - this.enableCustomInput = false; - } - }, - watch: { - cronInterval() { - // updates field validation state when model changes, as - // glFieldError only updates on input. - Vue.nextTick(() => { - gl.pipelineScheduleFieldErrors.updateFormValidityState(); - }); - }, - }, - template: ` - <div class="interval-pattern-form-group"> - <div class="cron-preset-radio-input"> - <input - id="custom" - class="label-light" - type="radio" - :name="inputNameAttribute" - :value="cronInterval" - :checked="isEditable" - @click="toggleCustomInput(true)" - /> - - <label for="custom"> - {{ s__('PipelineSheduleIntervalPattern|Custom') }} - </label> - - <span class="cron-syntax-link-wrap"> - (<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>) - </span> - </div> - - <div class="cron-preset-radio-input"> - <input - id="every-day" - class="label-light" - type="radio" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyDay" - @click="toggleCustomInput(false)" - /> - - <label class="label-light" for="every-day"> - {{ __('Every day (at 4:00am)') }} - </label> - </div> - - <div class="cron-preset-radio-input"> - <input - id="every-week" - class="label-light" - type="radio" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyWeek" - @click="toggleCustomInput(false)" - /> - - <label class="label-light" for="every-week"> - {{ __('Every week (Sundays at 4:00am)') }} - </label> - </div> - - <div class="cron-preset-radio-input"> - <input - id="every-month" - class="label-light" - type="radio" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyMonth" - @click="toggleCustomInput(false)" - /> - - <label class="label-light" for="every-month"> - {{ __('Every month (on the 1st at 4:00am)') }} - </label> - </div> - - <div class="cron-interval-input-wrapper"> - <input - id="schedule_cron" - class="form-control inline cron-interval-input" - type="text" - :placeholder="__('Define a custom pattern with cron syntax')" - required="true" - v-model="cronInterval" - :name="inputNameAttribute" - :disabled="!isEditable" - /> - </div> - </div> - `, -}; diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue new file mode 100644 index 00000000000..ce46b3fa3fa --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.vue @@ -0,0 +1,144 @@ +<script> + export default { + props: { + initialCronInterval: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + inputNameAttribute: 'schedule[cron]', + cronInterval: this.initialCronInterval, + cronIntervalPresets: { + everyDay: '0 4 * * *', + everyWeek: '0 4 * * 0', + everyMonth: '0 4 1 * *', + }, + cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', + customInputEnabled: false, + }; + }, + computed: { + intervalIsPreset() { + return _.contains(this.cronIntervalPresets, this.cronInterval); + }, + // The text input is editable when there's a custom interval, or when it's + // a preset interval and the user clicks the 'custom' radio button + isEditable() { + return !!(this.customInputEnabled || !this.intervalIsPreset); + }, + }, + methods: { + toggleCustomInput(shouldEnable) { + this.customInputEnabled = shouldEnable; + + if (shouldEnable) { + // We need to change the value so other radios don't remain selected + // because the model (cronInterval) hasn't changed. The server trims it. + this.cronInterval = `${this.cronInterval} `; + } + }, + }, + created() { + if (this.intervalIsPreset) { + this.enableCustomInput = false; + } + }, + watch: { + cronInterval() { + // updates field validation state when model changes, as + // glFieldError only updates on input. + this.$nextTick(() => { + gl.pipelineScheduleFieldErrors.updateFormValidityState(); + }); + }, + }, + }; +</script> + +<template> + <div class="interval-pattern-form-group"> + <div class="cron-preset-radio-input"> + <input + id="custom" + class="label-light" + type="radio" + :name="inputNameAttribute" + :value="cronInterval" + :checked="isEditable" + @click="toggleCustomInput(true)" + /> + + <label for="custom"> + {{ s__('PipelineSheduleIntervalPattern|Custom') }} + </label> + + <span class="cron-syntax-link-wrap"> + (<a :href="cronSyntaxUrl" target="_blank">{{ __('Cron syntax') }}</a>) + </span> + </div> + + <div class="cron-preset-radio-input"> + <input + id="every-day" + class="label-light" + type="radio" + v-model="cronInterval" + :name="inputNameAttribute" + :value="cronIntervalPresets.everyDay" + @click="toggleCustomInput(false)" + /> + + <label class="label-light" for="every-day"> + {{ __('Every day (at 4:00am)') }} + </label> + </div> + + <div class="cron-preset-radio-input"> + <input + id="every-week" + class="label-light" + type="radio" + v-model="cronInterval" + :name="inputNameAttribute" + :value="cronIntervalPresets.everyWeek" + @click="toggleCustomInput(false)" + /> + + <label class="label-light" for="every-week"> + {{ __('Every week (Sundays at 4:00am)') }} + </label> + </div> + + <div class="cron-preset-radio-input"> + <input + id="every-month" + class="label-light" + type="radio" + v-model="cronInterval" + :name="inputNameAttribute" + :value="cronIntervalPresets.everyMonth" + @click="toggleCustomInput(false)" + /> + + <label class="label-light" for="every-month"> + {{ __('Every month (on the 1st at 4:00am)') }} + </label> + </div> + + <div class="cron-interval-input-wrapper"> + <input + id="schedule_cron" + class="form-control inline cron-interval-input" + type="text" + :placeholder="__('Define a custom pattern with cron syntax')" + required="true" + v-model="cronInterval" + :name="inputNameAttribute" + :disabled="!isEditable" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js index c60e77decce..50c725aa3d5 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -1,21 +1,45 @@ import Vue from 'vue'; -import IntervalPatternInput from './components/interval_pattern_input'; +import Translate from '../vue_shared/translate'; +import intervalPatternInput from './components/interval_pattern_input.vue'; import TimezoneDropdown from './components/timezone_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown'; +import { setupPipelineVariableList } from './setup_pipeline_variable_list'; -document.addEventListener('DOMContentLoaded', () => { - const IntervalPatternInputComponent = Vue.extend(IntervalPatternInput); +Vue.use(Translate); + +function initIntervalPatternInput() { const intervalPatternMount = document.getElementById('interval-pattern-input'); const initialCronInterval = intervalPatternMount ? intervalPatternMount.dataset.initialInterval : ''; - new IntervalPatternInputComponent({ - propsData: { - initialCronInterval, + return new Vue({ + el: intervalPatternMount, + components: { + intervalPatternInput, + }, + render(createElement) { + return createElement('interval-pattern-input', { + props: { + initialCronInterval, + }, + }); }, - }).$mount(intervalPatternMount); + }); +} + +document.addEventListener('DOMContentLoaded', () => { + /* Most of the form is written in haml, but for fields with more complex behaviors, + * you should mount individual Vue components here. If at some point components need + * to share state, it may make sense to refactor the whole form to Vue */ + + initIntervalPatternInput(); + + // Initialize non-Vue JS components in the form const formElement = document.getElementById('new-pipeline-schedule-form'); + gl.timezoneDropdown = new TimezoneDropdown(); gl.targetBranchDropdown = new TargetBranchDropdown(); gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); + + setupPipelineVariableList($('.js-pipeline-variable-list')); }); diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js new file mode 100644 index 00000000000..644efd10509 --- /dev/null +++ b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js @@ -0,0 +1,71 @@ +function insertRow($row) { + const $rowClone = $row.clone(); + $rowClone.removeAttr('data-is-persisted'); + $rowClone.find('input, textarea').val(''); + $row.after($rowClone); +} + +function removeRow($row) { + const isPersisted = gl.utils.convertPermissionToBoolean($row.attr('data-is-persisted')); + + if (isPersisted) { + $row.hide(); + $row + .find('.js-destroy-input') + .val(1); + } else { + $row.remove(); + } +} + +function checkIfRowTouched($row) { + return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0); +} + +function setupPipelineVariableList(parent = document) { + const $parent = $(parent); + + $parent.on('click', '.js-row-remove-button', (e) => { + const $row = $(e.currentTarget).closest('.js-row'); + removeRow($row); + + e.preventDefault(); + }); + + // Remove any empty rows except the last r + $parent.on('blur', '.js-user-input', (e) => { + const $row = $(e.currentTarget).closest('.js-row'); + + const isTouched = checkIfRowTouched($row); + if ($row.is(':not(:last-child)') && !isTouched) { + removeRow($row); + } + }); + + // Always make sure there is an empty last row + $parent.on('input', '.js-user-input', () => { + const $lastRow = $parent.find('.js-row').last(); + + const isTouched = checkIfRowTouched($lastRow); + if (isTouched) { + insertRow($lastRow); + } + }); + + // Clear out the empty last row so it + // doesn't get submitted and throw validation errors + $parent.closest('form').on('submit', () => { + const $lastRow = $parent.find('.js-row').last(); + + const isTouched = checkIfRowTouched($lastRow); + if (!isTouched) { + $lastRow.find('input, textarea').attr('name', ''); + } + }); +} + +export { + setupPipelineVariableList, + insertRow, + removeRow, +}; diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue index abcd0c4ecea..16cc0761fc1 100644 --- a/app/assets/javascripts/pipelines/components/async_button.vue +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -3,7 +3,7 @@ import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltipMixin from '../../vue_shared/mixins/tooltip'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -28,12 +28,12 @@ export default { required: false, }, }, + directives: { + tooltip, + }, components: { loadingIcon, }, - mixins: [ - tooltipMixin, - ], data() { return { isLoading: false, @@ -58,7 +58,6 @@ export default { makeRequest() { this.isLoading = true; - $(this.$refs.tooltip).tooltip('destroy'); eventHub.$emit('postAction', this.endpoint); }, }, @@ -67,6 +66,7 @@ export default { <template> <button + v-tooltip type="button" @click="onClick" :class="buttonClass" @@ -74,7 +74,6 @@ export default { :aria-label="title" data-container="body" data-placement="top" - ref="tooltip" :disabled="isLoading"> <i :class="iconClass" diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 1f9e3d39779..54227425d2a 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -1,6 +1,6 @@ <script> import getActionIcon from '../../../vue_shared/ci_action_icons'; - import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + import tooltip from '../../../vue_shared/directives/tooltip'; /** * Renders either a cancel, retry or play icon pointing to the given path. @@ -29,9 +29,9 @@ }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, computed: { actionIconSvg() { @@ -46,12 +46,11 @@ </script> <template> <a + v-tooltip :data-method="actionMethod" :title="tooltipText" :href="link" - ref="tooltip" class="ci-action-icon-container" - data-toggle="tooltip" data-container="body"> <i diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue index 19cafff4e1c..18fe1847eef 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_action_component.vue @@ -1,6 +1,6 @@ <script> import getActionIcon from '../../../vue_shared/ci_action_icons'; - import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + import tooltip from '../../../vue_shared/directives/tooltip'; /** * Renders either a cancel, retry or play icon pointing to the given path. @@ -29,9 +29,9 @@ }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, computed: { actionIconSvg() { @@ -42,13 +42,12 @@ </script> <template> <a + v-tooltip :data-method="actionMethod" :title="tooltipText" :href="link" - ref="tooltip" rel="nofollow" class="ci-action-icon-wrapper js-ci-status-icon" - data-toggle="tooltip" data-container="body" v-html="actionIconSvg" aria-label="Job's action"> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index d597af8dfb5..2944689a5a7 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -1,7 +1,7 @@ <script> import jobNameComponent from './job_name_component.vue'; import jobComponent from './job_component.vue'; - import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + import tooltip from '../../../vue_shared/directives/tooltip'; /** * Renders the dropdown for the pipeline graph. @@ -34,9 +34,9 @@ }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, components: { jobComponent, @@ -53,12 +53,12 @@ <template> <div> <button + v-tooltip type="button" data-toggle="dropdown" data-container="body" class="dropdown-menu-toggle build-content" - :title="tooltipText" - ref="tooltip"> + :title="tooltipText"> <job-name-component :name="job.name" diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index b39c936101e..1f5ed3f1074 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -2,7 +2,7 @@ import actionComponent from './action_component.vue'; import dropdownActionComponent from './dropdown_action_component.vue'; import jobNameComponent from './job_name_component.vue'; - import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + import tooltip from '../../../vue_shared/directives/tooltip'; /** * Renders the badge for the pipeline graph and the job's dropdown. @@ -54,9 +54,9 @@ jobNameComponent, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, computed: { tooltipText() { @@ -77,12 +77,11 @@ <template> <div> <a + v-tooltip v-if="job.status.details_path" :href="job.status.details_path" :title="tooltipText" :class="cssClassJobName" - ref="tooltip" - data-toggle="tooltip" data-container="body"> <job-name-component @@ -93,10 +92,9 @@ <div v-else + v-tooltip :title="tooltipText" :class="cssClassJobName" - ref="tooltip" - data-toggle="tooltip" data-container="body"> <job-name-component diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 8333ec0fbc3..2ca5ac2912f 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -1,6 +1,6 @@ <script> import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; -import tooltipMixin from '../../vue_shared/mixins/tooltip'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -12,9 +12,9 @@ export default { components: { userAvatarLink, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, computed: { user() { return this.pipeline.user; @@ -45,16 +45,16 @@ export default { <div class="label-container"> <span v-if="pipeline.flags.latest" + v-tooltip class="js-pipeline-url-latest label label-success" - title="Latest pipeline for this branch" - ref="tooltip"> + title="Latest pipeline for this branch"> latest </span> <span v-if="pipeline.flags.yaml_errors" + v-tooltip class="js-pipeline-url-yaml label label-danger" - :title="pipeline.yaml_errors" - ref="tooltip"> + :title="pipeline.yaml_errors"> yaml invalid </span> <span diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 01ae07aad65..5df317a76bf 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -129,14 +129,11 @@ }, successCallback(resp) { - const response = { - headers: resp.headers, - body: resp.json(), - }; - - this.store.storeCount(response.body.count); - this.store.storePagination(response.headers); - this.setCommonData(response.body.pipelines); + return resp.json().then((response) => { + this.store.storeCount(response.count); + this.store.storePagination(resp.headers); + this.setCommonData(response.pipelines); + }); }, }, }; diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index a6fc4f04237..01dfe51cc17 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -4,6 +4,7 @@ import playIconSvg from 'icons/_icon_play.svg'; import eventHub from '../event_hub'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -12,6 +13,9 @@ required: true, }, }, + directives: { + tooltip, + }, components: { loadingIcon, }, @@ -25,8 +29,6 @@ onClickAction(endpoint) { this.isLoading = true; - $(this.$refs.tooltip).tooltip('destroy'); - eventHub.$emit('postAction', endpoint); }, @@ -43,13 +45,13 @@ <template> <div class="btn-group"> <button + v-tooltip type="button" - class="dropdown-new btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" + class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" title="Manual job" data-toggle="dropdown" data-placement="top" aria-label="Manual job" - ref="tooltip" :disabled="isLoading"> <span v-html="playIconSvg"></span> <i diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index b4520481cdc..b19bd509a00 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -1,5 +1,5 @@ <script> - import tooltipMixin from '../../vue_shared/mixins/tooltip'; + import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -8,9 +8,9 @@ required: true, }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, }; </script> <template> @@ -18,12 +18,12 @@ class="btn-group" role="group"> <button + v-tooltip class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download" title="Artifacts" data-placement="top" data-toggle="dropdown" - aria-label="Artifacts" - ref="tooltip"> + aria-label="Artifacts"> <i class="fa fa-download" aria-hidden="true"> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index c05c76c9a64..a4a27247406 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -16,7 +16,7 @@ /* global Flash */ import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; -import tooltipMixin from '../../vue_shared/mixins/tooltip'; +import tooltip from '../../vue_shared/directives/tooltip'; export default { props: { @@ -32,15 +32,14 @@ export default { }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, data() { return { isLoading: false, dropdownContent: '', - endpoint: this.stage.dropdown_path, }; }, @@ -73,9 +72,10 @@ export default { }, fetchJobs() { - this.$http.get(this.endpoint) - .then((response) => { - this.dropdownContent = response.json().html; + this.$http.get(this.stage.dropdown_path) + .then(response => response.json()) + .then((data) => { + this.dropdownContent = data.html; this.isLoading = false; }) .catch(() => { @@ -132,7 +132,7 @@ export default { <template> <div class="dropdown"> <button - ref="tooltip" + v-tooltip :class="triggerButtonClass" @click="onClickStage" class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index be3f32afa09..037684b4e72 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -1,7 +1,7 @@ <script> import iconTimerSvg from 'icons/_icon_timer.svg'; import '../../lib/utils/datetime_utility'; - import tooltipMixin from '../../vue_shared/mixins/tooltip'; + import tooltip from '../../vue_shared/directives/tooltip'; import timeagoMixin from '../../vue_shared/mixins/timeago'; export default { @@ -16,9 +16,11 @@ }, }, mixins: [ - tooltipMixin, timeagoMixin, ], + directives: { + tooltip, + }, data() { return { iconTimerSvg, @@ -81,7 +83,7 @@ </i> <time - ref="tooltip" + v-tooltip data-placement="top" data-container="body" :title="tooltipTitle(finishedTime)"> diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js index 82537ea06f5..385e7430a7d 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js +++ b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js @@ -40,10 +40,10 @@ export default class pipelinesMediator { } successCallback(response) { - const data = response.json(); - - this.state.isLoading = false; - this.store.storePipeline(data); + return response.json().then((data) => { + this.state.isLoading = false; + this.store.storePipeline(data); + }); } errorCallback() { diff --git a/app/assets/javascripts/pipelines/pipelines_charts.js b/app/assets/javascripts/pipelines/pipelines_charts.js new file mode 100644 index 00000000000..001faf4be33 --- /dev/null +++ b/app/assets/javascripts/pipelines/pipelines_charts.js @@ -0,0 +1,38 @@ +import Chart from 'vendor/Chart'; + +document.addEventListener('DOMContentLoaded', () => { + const chartData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); + const buildChart = (chartScope) => { + const data = { + labels: chartScope.labels, + datasets: [{ + fillColor: '#7f8fa4', + strokeColor: '#7f8fa4', + pointColor: '#7f8fa4', + pointStrokeColor: '#EEE', + data: chartScope.totalValues, + }, + { + fillColor: '#44aa22', + strokeColor: '#44aa22', + pointColor: '#44aa22', + pointStrokeColor: '#fff', + data: chartScope.successValues, + }, + ], + }; + const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d'); + const options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false, + }; + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8; + } + new Chart(ctx).Line(data, options); + }; + + chartData.forEach(scope => buildChart(scope)); +}); diff --git a/app/assets/javascripts/pipelines/pipelines_times.js b/app/assets/javascripts/pipelines/pipelines_times.js new file mode 100644 index 00000000000..b5e7a0e53d9 --- /dev/null +++ b/app/assets/javascripts/pipelines/pipelines_times.js @@ -0,0 +1,27 @@ +import Chart from 'vendor/Chart'; + +document.addEventListener('DOMContentLoaded', () => { + const chartData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML); + const data = { + labels: chartData.labels, + datasets: [{ + fillColor: 'rgba(220,220,220,0.5)', + strokeColor: 'rgba(220,220,220,1)', + barStrokeWidth: 1, + barValueSpacing: 1, + barDatasetSpacing: 1, + data: chartData.values, + }], + }; + const ctx = $('#build_timesChart').get(0).getContext('2d'); + const options = { + scaleOverlay: true, + responsive: true, + maintainAspectRatio: false, + }; + if (window.innerWidth < 768) { + // Scale fonts if window width lower than 768px (iPad portrait) + options.scaleFontSize = 8; + } + new Chart(ctx).Bar(data, options); +}); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 738e710deb9..a3f7d69b98d 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -6,21 +6,22 @@ import Cookies from 'js-cookie'; (function() { this.Project = (function() { function Project() { - $('ul.clone-options-dropdown a').click(function() { - var url; - if ($(this).hasClass('active')) { - return; - } - $('.active').not($(this)).removeClass('active'); - $(this).toggleClass('active'); - url = $("#project_clone").val(); - $('#project_clone').val(url); + const $cloneOptions = $('ul.clone-options-dropdown'); + const $projectCloneField = $('#project_clone'); + const $cloneBtnText = $('a.clone-dropdown-btn span'); + + $('a', $cloneOptions).on('click', (e) => { + const $this = $(e.currentTarget); + const url = $this.attr('href'); + + e.preventDefault(); + + $('.active', $cloneOptions).not($this).removeClass('active'); + $this.toggleClass('active'); + $projectCloneField.val(url); + $cloneBtnText.text($this.text()); + return $('.clone').text(url); - // Git protocol switcher - // Remove the active class for all buttons (ssh, http, kerberos if shown) - // Add the active class for the clicked button - // Update the input field - // Update the command line instructions }); // Ref switcher this.initRefSwitcher(); diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index c0f757269cb..fd89a1a85c3 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -1,5 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */ +import VisibilitySelect from './visibility_select'; + function highlightChanges($elm) { $elm.addClass('highlight-changes'); setTimeout(() => $elm.removeClass('highlight-changes'), 10); @@ -30,7 +32,7 @@ function highlightChanges($elm) { ProjectNew.prototype.initVisibilitySelect = function() { const visibilityContainer = document.querySelector('.js-visibility-select'); if (!visibilityContainer) return; - const visibilitySelect = new gl.VisibilitySelect(visibilityContainer); + const visibilitySelect = new VisibilitySelect(visibilityContainer); visibilitySelect.init(); const $visibilitySelect = $(visibilityContainer).find('select'); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 9896b88d487..ebcefc819f5 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -104,6 +104,14 @@ import Api from './api'; dropdownCssClass: "ajax-project-dropdown" }); }); + + $('.new-project-item-select-button').on('click', function() { + $('.project-item-select', this.parentNode).select2('open'); + }); + + $('.project-item-select').on('click', function() { + window.location = `${$(this).val()}/${this.dataset.relativePath}`; + }); } return ProjectSelect; diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js new file mode 100644 index 00000000000..2091b275c3d --- /dev/null +++ b/app/assets/javascripts/projects/project_new.js @@ -0,0 +1,43 @@ +document.addEventListener('DOMContentLoaded', () => { + const importBtnTooltip = 'Please enter a valid project name.'; + const $importBtnWrapper = $('.import_gitlab_project'); + + $('.how_to_import_link').on('click', (e) => { + e.preventDefault(); + $('.how_to_import_link').next('.modal').show(); + }); + + $('.modal-header .close').on('click', () => { + $('.modal').hide(); + }); + + $('.btn_import_gitlab_project').on('click', () => { + const importHref = $('a.btn_import_gitlab_project').attr('href'); + $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$('#project_path').val()}`); + }); + + $('.btn_import_gitlab_project').attr('disabled', !$('#project_path').val().trim().length); + $importBtnWrapper.attr('title', importBtnTooltip); + + $('#new_project').on('submit', () => { + const $path = $('#project_path'); + $path.val($path.val().trim()); + }); + + $('#project_path').on('keyup', () => { + if ($('#project_path').val().trim().length) { + $('.btn_import_gitlab_project').attr('disabled', false); + $importBtnWrapper.attr('title', ''); + $importBtnWrapper.removeClass('has-tooltip'); + } else { + $('.btn_import_gitlab_project').attr('disabled', true); + $importBtnWrapper.addClass('has-tooltip'); + } + }); + + $('#project_import_url').disable(); + $('.import_git').on('click', () => { + const $projectImportUrl = $('#project_import_url'); + $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled')); + }); +}); diff --git a/app/assets/javascripts/prometheus_metrics/constants.js b/app/assets/javascripts/prometheus_metrics/constants.js new file mode 100644 index 00000000000..50f1248456e --- /dev/null +++ b/app/assets/javascripts/prometheus_metrics/constants.js @@ -0,0 +1,5 @@ +export default { + EMPTY: 'empty', + LOADING: 'loading', + LIST: 'list', +}; diff --git a/app/assets/javascripts/prometheus_metrics/index.js b/app/assets/javascripts/prometheus_metrics/index.js new file mode 100644 index 00000000000..a0c43c5abe1 --- /dev/null +++ b/app/assets/javascripts/prometheus_metrics/index.js @@ -0,0 +1,6 @@ +import PrometheusMetrics from './prometheus_metrics'; + +$(() => { + const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + prometheusMetrics.loadActiveMetrics(); +}); diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js new file mode 100644 index 00000000000..ef4d6df5138 --- /dev/null +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -0,0 +1,109 @@ +import PANEL_STATE from './constants'; + +export default class PrometheusMetrics { + constructor(wrapperSelector) { + this.backOffRequestCounter = 0; + + this.$wrapper = $(wrapperSelector); + + this.$monitoredMetricsPanel = this.$wrapper.find('.js-panel-monitored-metrics'); + this.$monitoredMetricsCount = this.$monitoredMetricsPanel.find('.js-monitored-count'); + this.$monitoredMetricsLoading = this.$monitoredMetricsPanel.find('.js-loading-metrics'); + this.$monitoredMetricsEmpty = this.$monitoredMetricsPanel.find('.js-empty-metrics'); + this.$monitoredMetricsList = this.$monitoredMetricsPanel.find('.js-metrics-list'); + + this.$missingEnvVarPanel = this.$wrapper.find('.js-panel-missing-env-vars'); + this.$panelToggle = this.$missingEnvVarPanel.find('.js-panel-toggle'); + this.$missingEnvVarMetricCount = this.$missingEnvVarPanel.find('.js-env-var-count'); + this.$missingEnvVarMetricsList = this.$missingEnvVarPanel.find('.js-missing-var-metrics-list'); + + this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('active-metrics'); + + this.$panelToggle.on('click', e => this.handlePanelToggle(e)); + } + + /* eslint-disable class-methods-use-this */ + handlePanelToggle(e) { + const $toggleBtn = $(e.currentTarget); + const $currentPanelBody = $toggleBtn.closest('.panel').find('.panel-body'); + $currentPanelBody.toggleClass('hidden'); + if ($toggleBtn.hasClass('fa-caret-down')) { + $toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right'); + } else { + $toggleBtn.removeClass('fa-caret-right').addClass('fa-caret-down'); + } + } + + showMonitoringMetricsPanelState(stateName) { + switch (stateName) { + case PANEL_STATE.LOADING: + this.$monitoredMetricsLoading.removeClass('hidden'); + this.$monitoredMetricsEmpty.addClass('hidden'); + this.$monitoredMetricsList.addClass('hidden'); + break; + case PANEL_STATE.LIST: + this.$monitoredMetricsLoading.addClass('hidden'); + this.$monitoredMetricsEmpty.addClass('hidden'); + this.$monitoredMetricsList.removeClass('hidden'); + break; + default: + this.$monitoredMetricsLoading.addClass('hidden'); + this.$monitoredMetricsEmpty.removeClass('hidden'); + this.$monitoredMetricsList.addClass('hidden'); + break; + } + } + + populateActiveMetrics(metrics) { + let totalMonitoredMetrics = 0; + let totalMissingEnvVarMetrics = 0; + + metrics.forEach((metric) => { + this.$monitoredMetricsList.append(`<li>${metric.group}<span class="badge">${metric.active_metrics}</span></li>`); + totalMonitoredMetrics += metric.active_metrics; + if (metric.metrics_missing_requirements > 0) { + this.$missingEnvVarMetricsList.append(`<li>${metric.group}</li>`); + totalMissingEnvVarMetrics += 1; + } + }); + + this.$monitoredMetricsCount.text(totalMonitoredMetrics); + this.showMonitoringMetricsPanelState(PANEL_STATE.LIST); + + if (totalMissingEnvVarMetrics > 0) { + this.$missingEnvVarPanel.removeClass('hidden'); + this.$missingEnvVarPanel.find('.flash-container').off('click'); + this.$missingEnvVarMetricCount.text(totalMissingEnvVarMetrics); + } + } + + loadActiveMetrics() { + this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); + gl.utils.backOff((next, stop) => { + $.getJSON(this.activeMetricsEndpoint) + .done((res) => { + if (res && res.success) { + stop(res); + } else { + this.backOffRequestCounter = this.backOffRequestCounter += 1; + if (this.backOffRequestCounter < 3) { + next(); + } else { + stop(res); + } + } + }) + .fail(stop); + }) + .then((res) => { + if (res && res.data && res.data.length) { + this.populateActiveMetrics(res.data); + } else { + this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); + } + }) + .catch(() => { + this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); + }); + } +} diff --git a/app/assets/javascripts/protected_branches/index.js b/app/assets/javascripts/protected_branches/index.js new file mode 100644 index 00000000000..c9e7af127d2 --- /dev/null +++ b/app/assets/javascripts/protected_branches/index.js @@ -0,0 +1,9 @@ +/* eslint-disable no-unused-vars */ + +import ProtectedBranchCreate from './protected_branch_create'; +import ProtectedBranchEditList from './protected_branch_edit_list'; + +$(() => { + const protectedBranchCreate = new ProtectedBranchCreate(); + const protectedBranchEditList = new ProtectedBranchEditList(); +}); diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js index 42993a252c3..38b1406a99f 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js @@ -1,31 +1,26 @@ -/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, max-len */ +export default class ProtectedBranchAccessDropdown { + constructor(options) { + this.options = options; + this.initDropdown(); + } -(global => { - global.gl = global.gl || {}; - - gl.ProtectedBranchAccessDropdown = class { - constructor(options) { - const { $dropdown, data, onSelect } = options; - - $dropdown.glDropdown({ - data: data, - selectable: true, - inputId: $dropdown.data('input-id'), - fieldName: $dropdown.data('field-name'), - toggleLabel(item, el) { - if (el.is('.is-active')) { - return item.text; - } else { - return 'Select'; - } - }, - clicked(opts) { - const { e } = opts; - - e.preventDefault(); - onSelect(); + initDropdown() { + const { $dropdown, data, onSelect } = this.options; + $dropdown.glDropdown({ + data, + selectable: true, + inputId: $dropdown.data('input-id'), + fieldName: $dropdown.data('field-name'), + toggleLabel(item, $el) { + if ($el.is('.is-active')) { + return item.text; } - }); - } - }; -})(window); + return 'Select'; + }, + clicked(options) { + options.e.preventDefault(); + onSelect(); + }, + }); + } +} diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 57ea2f52814..10da3783123 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -1,55 +1,51 @@ -/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */ -/* global ProtectedBranchDropdown */ - -(global => { - global.gl = global.gl || {}; - - gl.ProtectedBranchCreate = class { - constructor() { - this.$wrap = this.$form = $('#new_protected_branch'); - this.buildDropdowns(); - } - - buildDropdowns() { - const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); - const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); - - // Cache callback - this.onSelectCallback = this.onSelect.bind(this); - - // Allowed to Merge dropdown - new gl.ProtectedBranchAccessDropdown({ - $dropdown: $allowedToMergeDropdown, - data: gon.merge_access_levels, - onSelect: this.onSelectCallback - }); - - // Allowed to Push dropdown - new gl.ProtectedBranchAccessDropdown({ - $dropdown: $allowedToPushDropdown, - data: gon.push_access_levels, - onSelect: this.onSelectCallback - }); - - // Select default - $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); - $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); - - // Protected branch dropdown - new ProtectedBranchDropdown({ - $dropdown: this.$wrap.find('.js-protected-branch-select'), - onSelect: this.onSelectCallback - }); - } - - // This will run after clicked callback - onSelect() { - // Enable submit button - const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); - const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); - const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); - - this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length)); - } - }; -})(window); +import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; +import ProtectedBranchDropdown from './protected_branch_dropdown'; + +export default class ProtectedBranchCreate { + constructor() { + this.$form = $('.js-new-protected-branch'); + this.buildDropdowns(); + } + + buildDropdowns() { + const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge'); + const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push'); + + // Cache callback + this.onSelectCallback = this.onSelect.bind(this); + + // Allowed to Merge dropdown + this.protectedBranchMergeAccessDropdown = new ProtectedBranchAccessDropdown({ + $dropdown: $allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelectCallback, + }); + + // Allowed to Push dropdown + this.protectedBranchPushAccessDropdown = new ProtectedBranchAccessDropdown({ + $dropdown: $allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelectCallback, + }); + + // Select default + $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); + $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); + + // Protected branch dropdown + this.protectedBranchDropdown = new ProtectedBranchDropdown({ + $dropdown: this.$form.find('.js-protected-branch-select'), + onSelect: this.onSelectCallback, + }); + } + + // This will run after clicked callback + onSelect() { + // Enable submit button + const $branchInput = this.$form.find('input[name="protected_branch[name]"]'); + const $allowedToMergeInput = this.$form.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); + const $allowedToPushInput = this.$form.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]'); + + this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length)); + } +} diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js index bc6110fcd4e..cc0b2ebe071 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js @@ -1,6 +1,10 @@ -/* eslint-disable comma-dangle, no-unused-vars */ - -class ProtectedBranchDropdown { +export default class ProtectedBranchDropdown { + /** + * @param {Object} options containing + * `$dropdown` target element + * `onSelect` event callback + * $dropdown must be an element created using `dropdown_branch()` rails helper + */ constructor(options) { this.onSelect = options.onSelect; this.$dropdown = options.$dropdown; @@ -12,7 +16,7 @@ class ProtectedBranchDropdown { this.bindEvents(); // Hide footer - this.$dropdownFooter.addClass('hidden'); + this.toggleFooter(true); } buildDropdown() { @@ -21,7 +25,7 @@ class ProtectedBranchDropdown { filterable: true, remote: false, search: { - fields: ['title'] + fields: ['title'], }, selectable: true, toggleLabel(selected) { @@ -36,10 +40,9 @@ class ProtectedBranchDropdown { }, onFilter: this.toggleCreateNewButton.bind(this), clicked: (options) => { - const { $el, e } = options; - e.preventDefault(); + options.e.preventDefault(); this.onSelect(); - } + }, }); } @@ -64,20 +67,22 @@ class ProtectedBranchDropdown { } toggleCreateNewButton(branchName) { - this.selectedBranch = { - title: branchName, - id: branchName, - text: branchName - }; - if (branchName) { + this.selectedBranch = { + title: branchName, + id: branchName, + text: branchName, + }; + this.$dropdownContainer .find('.js-create-new-protected-branch code') .text(branchName); } - this.$dropdownFooter.toggleClass('hidden', !branchName); + this.toggleFooter(!branchName); } -} -window.ProtectedBranchDropdown = ProtectedBranchDropdown; + toggleFooter(toggleState) { + this.$dropdownFooter.toggleClass('hidden', toggleState); + } +} diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 6ef59e94384..3b920942a3f 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -1,69 +1,67 @@ -/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */ +/* eslint-disable no-new */ /* global Flash */ -(global => { - global.gl = global.gl || {}; +import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; - gl.ProtectedBranchEdit = class { - constructor(options) { - this.$wrap = options.$wrap; - this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); - this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); +export default class ProtectedBranchEdit { + constructor(options) { + this.$wrap = options.$wrap; + this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + this.onSelectCallback = this.onSelect.bind(this); - this.buildDropdowns(); - } + this.buildDropdowns(); + } - buildDropdowns() { - // Allowed to merge dropdown - new gl.ProtectedBranchAccessDropdown({ - $dropdown: this.$allowedToMergeDropdown, - data: gon.merge_access_levels, - onSelect: this.onSelect.bind(this) - }); + buildDropdowns() { + // Allowed to merge dropdown + this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelectCallback, + }); - // Allowed to push dropdown - new gl.ProtectedBranchAccessDropdown({ - $dropdown: this.$allowedToPushDropdown, - data: gon.push_access_levels, - onSelect: this.onSelect.bind(this) - }); - } + // Allowed to push dropdown + this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelectCallback, + }); + } - onSelect() { - const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); - const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + onSelect() { + const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); + const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); - // Do not update if one dropdown has not selected any option - if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + // Do not update if one dropdown has not selected any option + if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; - this.$allowedToMergeDropdown.disable(); - this.$allowedToPushDropdown.disable(); + this.$allowedToMergeDropdown.disable(); + this.$allowedToPushDropdown.disable(); - $.ajax({ - type: 'POST', - url: this.$wrap.data('url'), - dataType: 'json', - data: { - _method: 'PATCH', - protected_branch: { - merge_access_levels_attributes: [{ - id: this.$allowedToMergeDropdown.data('access-level-id'), - access_level: $allowedToMergeInput.val() - }], - push_access_levels_attributes: [{ - id: this.$allowedToPushDropdown.data('access-level-id'), - access_level: $allowedToPushInput.val() - }] - } + $.ajax({ + type: 'POST', + url: this.$wrap.data('url'), + dataType: 'json', + data: { + _method: 'PATCH', + protected_branch: { + merge_access_levels_attributes: [{ + id: this.$allowedToMergeDropdown.data('access-level-id'), + access_level: $allowedToMergeInput.val(), + }], + push_access_levels_attributes: [{ + id: this.$allowedToPushDropdown.data('access-level-id'), + access_level: $allowedToPushInput.val(), + }], }, - error() { - $.scrollTo(0); - new Flash('Failed to update branch!'); - } - }).always(() => { - this.$allowedToMergeDropdown.enable(); - this.$allowedToPushDropdown.enable(); - }); - } - }; -})(window); + }, + error() { + new Flash('Failed to update branch!', null, $('.js-protected-branches-list')); + }, + }).always(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + }); + } +} diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js index 336fa6c57a7..b40d3827c30 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js @@ -1,18 +1,18 @@ -/* eslint-disable arrow-parens, no-param-reassign, no-new, comma-dangle */ +/* eslint-disable no-new */ -(global => { - global.gl = global.gl || {}; +import ProtectedBranchEdit from './protected_branch_edit'; - gl.ProtectedBranchEditList = class { - constructor() { - this.$wrap = $('.protected-branches-list'); +export default class ProtectedBranchEditList { + constructor() { + this.$wrap = $('.protected-branches-list'); + this.initEditForm(); + } - // Build edit forms - this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { - new gl.ProtectedBranchEdit({ - $wrap: $(el) - }); + initEditForm() { + this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { + new ProtectedBranchEdit({ + $wrap: $(el), }); - } - }; -})(window); + }); + } +} diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js deleted file mode 100644 index 874d70a1431..00000000000 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ /dev/null @@ -1,5 +0,0 @@ -import './protected_branch_access_dropdown'; -import './protected_branch_create'; -import './protected_branch_dropdown'; -import './protected_branch_edit'; -import './protected_branch_edit_list'; diff --git a/app/assets/javascripts/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js index 61e7ba53862..b1618e24e49 100644 --- a/app/assets/javascripts/protected_tags/index.js +++ b/app/assets/javascripts/protected_tags/index.js @@ -1,2 +1,9 @@ -export { default as ProtectedTagCreate } from './protected_tag_create'; -export { default as ProtectedTagEditList } from './protected_tag_edit_list'; +/* eslint-disable no-unused-vars */ + +import ProtectedTagCreate from './protected_tag_create'; +import ProtectedTagEditList from './protected_tag_edit_list'; + +$(() => { + const protectedtTagCreate = new ProtectedTagCreate(); + const protectedtTagEditList = new ProtectedTagEditList(); +}); diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js index 215cd6fbdfd..65e4101352c 100644 --- a/app/assets/javascripts/ref_select_dropdown.js +++ b/app/assets/javascripts/ref_select_dropdown.js @@ -1,7 +1,8 @@ class RefSelectDropdown { constructor($dropdownButton, availableRefs) { + const availableRefsValue = availableRefs || JSON.parse(document.getElementById('availableRefs').innerHTML); $dropdownButton.glDropdown({ - data: availableRefs, + data: availableRefsValue, filterable: true, filterByText: true, remote: false, diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index da7c0c5a36c..d8f1fe10b26 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, max-len */ import Cookies from 'js-cookie'; +import SidebarHeightManager from './sidebar_height_manager'; (function() { this.Sidebar = (function() { @@ -8,12 +9,6 @@ import Cookies from 'js-cookie'; this.toggleTodo = this.toggleTodo.bind(this); this.sidebar = $('aside'); - this.$sidebarInner = this.sidebar.find('.issuable-sidebar'); - this.$navGitlab = $('.navbar-gitlab'); - this.$layoutNav = $('.layout-nav'); - this.$subScroll = $('.sub-nav-scroll'); - this.$rightSidebar = $('.js-right-sidebar'); - this.removeListeners(); this.addEventListeners(); } @@ -27,16 +22,14 @@ import Cookies from 'js-cookie'; }; Sidebar.prototype.addEventListeners = function() { + SidebarHeightManager.init(); const $document = $(document); - const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight.bind(this), 20); - const debouncedSetSidebarHeight = _.debounce(this.setSidebarHeight.bind(this), 200); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); - $(window).on('resize', () => throttledSetSidebarHeight()); - $document.on('scroll', () => debouncedSetSidebarHeight()); + $document.on('click', '.js-sidebar-toggle', function(e, triggered) { var $allGutterToggleIcons, $this, $thisIcon; e.preventDefault(); @@ -214,18 +207,6 @@ import Cookies from 'js-cookie'; } }; - Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0); - const diff = $navHeight - $(window).scrollTop(); - if (diff > 0) { - this.$rightSidebar.outerHeight($(window).height() - diff); - this.$sidebarInner.height('100%'); - } else { - this.$rightSidebar.outerHeight('100%'); - this.$sidebarInner.height(''); - } - }; - Sidebar.prototype.isOpen = function() { return this.sidebar.is('.right-sidebar-expanded'); }; diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index 59ff2a86293..7fa5996d600 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -4,7 +4,7 @@ function expandSectionParent($section, $content) { } function expandSection($section) { - $section.find('.js-settings-toggle').text('Close'); + $section.find('.js-settings-toggle').text('Collapse'); const $content = $section.find('.settings-content'); $content.addClass('expanded').off('scroll.expandSection').scrollTop(0); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index a4a7f3fa944..e3daa8cf949 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ /* global Mousetrap */ -/* global findFileURL */ import Cookies from 'js-cookie'; import findAndFollowLink from './shortcuts_dashboard_navigation'; @@ -20,6 +19,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; const $globalDropdownMenu = $('.global-dropdown-menu'); const $globalDropdownToggle = $('.global-dropdown-toggle'); + const findFileURL = document.body.dataset.findFile; $('.global-dropdown').on('hide.bs.dropdown', () => { $globalDropdownMenu.removeClass('shortcuts'); @@ -62,7 +62,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation'; if (Cookies.get(performanceBarCookieName) === 'true') { Cookies.remove(performanceBarCookieName, { path: '/' }); } else { - Cookies.set(performanceBarCookieName, true, { path: '/' }); + Cookies.set(performanceBarCookieName, 'true', { path: '/' }); } gl.utils.refreshCurrentPage(); }; diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js index a9ad3708514..5a6e47e566e 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.js +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.js @@ -14,6 +14,11 @@ export default { type: Boolean, required: true, }, + showToggle: { + type: Boolean, + required: false, + default: false, + }, }, computed: { assigneeTitle() { @@ -36,6 +41,19 @@ export default { > Edit </a> + <a + v-if="showToggle" + aria-label="Toggle sidebar" + class="gutter-toggle pull-right js-sidebar-toggle" + href="#" + role="button" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-angle-double-right" + /> + </a> </div> `, }; diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js index da4abf0b68f..f83c3b037ed 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js @@ -64,6 +64,7 @@ export default { }, beforeMount() { this.field = this.$el.dataset.field; + this.signedIn = typeof this.$el.dataset.signedIn !== 'undefined'; }, template: ` <div> @@ -71,6 +72,7 @@ export default { :number-of-assignees="store.assignees.length" :loading="loading || store.isFetching.assignees" :editable="store.editable" + :show-toggle="!signedIn" /> <assignees v-if="!store.isFetching.assignees" diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index 2b02af87d8a..a9df66748c5 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -5,7 +5,8 @@ import sidebarAssignees from './components/assignees/sidebar_assignees'; import Mediator from './sidebar_mediator'; function domContentLoaded() { - const mediator = new Mediator(gl.sidebarOptions); + const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); + const mediator = new Mediator(sidebarOptions); mediator.fetch(); const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees'); diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index 5ccfb4ee9c1..721e92221cf 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -28,8 +28,8 @@ export default class SidebarMediator { fetch() { this.service.get() - .then((response) => { - const data = response.json(); + .then(response => response.json()) + .then((data) => { this.store.setAssigneeData(data); this.store.setTimeTrackingData(data); }) diff --git a/app/assets/javascripts/sidebar_height_manager.js b/app/assets/javascripts/sidebar_height_manager.js new file mode 100644 index 00000000000..022415f22b2 --- /dev/null +++ b/app/assets/javascripts/sidebar_height_manager.js @@ -0,0 +1,33 @@ +export default { + init() { + if (!this.initialized) { + this.$window = $(window); + this.$rightSidebar = $('.js-right-sidebar'); + this.$navHeight = $('.navbar-gitlab').outerHeight() + + $('.layout-nav').outerHeight() + + $('.sub-nav-scroll').outerHeight(); + + const throttledSetSidebarHeight = _.throttle(() => this.setSidebarHeight(), 20); + const debouncedSetSidebarHeight = _.debounce(() => this.setSidebarHeight(), 200); + + this.$window.on('scroll', throttledSetSidebarHeight); + this.$window.on('resize', debouncedSetSidebarHeight); + this.initialized = true; + } + }, + + setSidebarHeight() { + const currentScrollDepth = window.pageYOffset || 0; + const diff = this.$navHeight - currentScrollDepth; + + if (diff > 0) { + const newSidebarHeight = window.innerHeight - diff; + this.$rightSidebar.outerHeight(newSidebarHeight); + this.sidebarHeightIsCustom = true; + } else if (this.sidebarHeightIsCustom) { + this.$rightSidebar.outerHeight('100%'); + this.sidebarHeightIsCustom = false; + } + }, +}; + diff --git a/app/assets/javascripts/signin_tabs_memoizer.js b/app/assets/javascripts/signin_tabs_memoizer.js index 2587facc582..20255398047 100644 --- a/app/assets/javascripts/signin_tabs_memoizer.js +++ b/app/assets/javascripts/signin_tabs_memoizer.js @@ -2,56 +2,52 @@ /* eslint no-new: "off" */ import AccessorUtilities from './lib/utils/accessor'; -((global) => { - /** - * Memorize the last selected tab after reloading a page. - * Does that setting the current selected tab in the localStorage - */ - class ActiveTabMemoizer { - constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) { - this.currentTabKey = currentTabKey; - this.tabSelector = tabSelector; - this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); - - this.bootstrap(); - } - - bootstrap() { - const tabs = document.querySelectorAll(this.tabSelector); - if (tabs.length > 0) { - tabs[0].addEventListener('click', (e) => { - if (e.target && e.target.nodeName === 'A') { - const anchorName = e.target.getAttribute('href'); - this.saveData(anchorName); - } - }); - } - - this.showTab(); - } +/** + * Memorize the last selected tab after reloading a page. + * Does that setting the current selected tab in the localStorage + */ +export default class SigninTabsMemoizer { + constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) { + this.currentTabKey = currentTabKey; + this.tabSelector = tabSelector; + this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + + this.bootstrap(); + } - showTab() { - const anchorName = this.readData(); - if (anchorName) { - const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`); - if (tab) { - tab.click(); + bootstrap() { + const tabs = document.querySelectorAll(this.tabSelector); + if (tabs.length > 0) { + tabs[0].addEventListener('click', (e) => { + if (e.target && e.target.nodeName === 'A') { + const anchorName = e.target.getAttribute('href'); + this.saveData(anchorName); } - } + }); } - saveData(val) { - if (!this.isLocalStorageAvailable) return undefined; + this.showTab(); + } - return window.localStorage.setItem(this.currentTabKey, val); + showTab() { + const anchorName = this.readData(); + if (anchorName) { + const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`); + if (tab) { + tab.click(); + } } + } - readData() { - if (!this.isLocalStorageAvailable) return null; + saveData(val) { + if (!this.isLocalStorageAvailable) return undefined; - return window.localStorage.getItem(this.currentTabKey); - } + return window.localStorage.setItem(this.currentTabKey, val); } - global.ActiveTabMemoizer = ActiveTabMemoizer; -})(window); + readData() { + if (!this.isLocalStorageAvailable) return null; + + return window.localStorage.getItem(this.currentTabKey); + } +} diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index c44892dae3d..4505a79a2df 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,96 +1,83 @@ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ -(function() { - window.SingleFileDiff = (function() { - var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; +import FilesCommentButton from './files_comment_button'; - WRAPPER = '<div class="diff-content"></div>'; +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. <a class="click-to-expand">Click to expand it.</a></div>'; - LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; - - ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; +export default class SingleFileDiff { + constructor(file) { + this.file = file; + this.toggleDiff = this.toggleDiff.bind(this); + this.content = $('.diff-content', this.file); + this.$toggleIcon = $('.diff-toggle-caret', this.file); + this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path'); + this.isOpen = !this.diffForPath; + if (this.diffForPath) { + this.collapsedContent = this.content; + 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.content.after(this.collapsedContent); + this.$toggleIcon.addClass('fa-caret-down'); + } - COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; + $('.js-file-title, .click-to-expand', this.file).on('click', (function (e) { + this.toggleDiff($(e.target)); + }).bind(this)); + } - function SingleFileDiff(file) { - this.file = file; - this.toggleDiff = this.toggleDiff.bind(this); - this.content = $('.diff-content', this.file); - this.$toggleIcon = $('.diff-toggle-caret', this.file); - this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path'); - this.isOpen = !this.diffForPath; - if (this.diffForPath) { - this.collapsedContent = this.content; - 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.content.after(this.collapsedContent); - this.$toggleIcon.addClass('fa-caret-down'); + toggleDiff($target, cb) { + 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(); + this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down'); + this.collapsedContent.show(); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); } - - $('.js-file-title, .click-to-expand', this.file).on('click', (function (e) { - this.toggleDiff($(e.target)); - }).bind(this)); + } else if (this.content) { + this.collapsedContent.hide(); + this.content.show(); + this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + } else { + this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); + return this.getContentHTML(cb); } + } - SingleFileDiff.prototype.toggleDiff = function($target, cb) { - 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(); - this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down'); - this.collapsedContent.show(); - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); + getContentHTML(cb) { + this.collapsedContent.hide(); + this.loadingContent.show(); + $.get(this.diffForPath, (function(_this) { + return function(data) { + _this.loadingContent.hide(); + if (data.html) { + _this.content = $(data.html); + _this.content.syntaxHighlight(); + } else { + _this.hasError = true; + _this.content = $(ERROR_HTML); } - } else if (this.content) { - this.collapsedContent.hide(); - this.content.show(); - this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); + _this.collapsedContent.after(_this.content); + if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); } - } else { - this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right'); - return this.getContentHTML(cb); - } - }; - - SingleFileDiff.prototype.getContentHTML = function(cb) { - this.collapsedContent.hide(); - this.loadingContent.show(); - $.get(this.diffForPath, (function(_this) { - return function(data) { - _this.loadingContent.hide(); - if (data.html) { - _this.content = $(data.html); - _this.content.syntaxHighlight(); - } else { - _this.hasError = true; - _this.content = $(ERROR_HTML); - } - _this.collapsedContent.after(_this.content); - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } + FilesCommentButton.init($(_this.file)); - if (cb) cb(); - }; - })(this)); - }; - - return SingleFileDiff; - })(); - - $.fn.singleFileDiff = function() { - return this.each(function() { - if (!$.data(this, 'singleFileDiff')) { - return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this)); - } - }); - }; -}).call(window); + if (cb) cb(); + }; + })(this)); + } +} diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js index d1bdc353be2..2bf7a3a5d61 100644 --- a/app/assets/javascripts/smart_interval.js +++ b/app/assets/javascripts/smart_interval.js @@ -1,158 +1,157 @@ -/* -* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable -* and controllable by a public API. -* -* */ - -(() => { - class SmartInterval { - /** - * @param { function } opts.callback Function to be called on each iteration (required) - * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially - * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this - * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this - * when the page is hidden - * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor - * @param { boolean } opts.lazyStart Configure if timer is initialized on - * instantiation or lazily - * @param { boolean } opts.immediateExecution Configure if callback should - * be executed before the first interval. - */ - constructor(opts = {}) { - this.cfg = { - callback: opts.callback, - startingInterval: opts.startingInterval, - maxInterval: opts.maxInterval, - hiddenInterval: opts.hiddenInterval, - incrementByFactorOf: opts.incrementByFactorOf, - lazyStart: opts.lazyStart, - immediateExecution: opts.immediateExecution, - }; - - this.state = { - intervalId: null, - currentInterval: this.cfg.startingInterval, - pageVisibility: 'visible', - }; - - this.initInterval(); - } - /* public */ - - start() { - const cfg = this.cfg; - const state = this.state; - - if (cfg.immediateExecution) { - cfg.immediateExecution = false; - cfg.callback(); - } +/** + * Instances of SmartInterval extend the functionality of `setInterval`, make it configurable + * and controllable by a public API. + */ + +class SmartInterval { + /** + * @param { function } opts.callback Function to be called on each iteration (required) + * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially + * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this + * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this + * when the page is hidden + * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor + * @param { boolean } opts.lazyStart Configure if timer is initialized on + * instantiation or lazily + * @param { boolean } opts.immediateExecution Configure if callback should + * be executed before the first interval. + */ + constructor(opts = {}) { + this.cfg = { + callback: opts.callback, + startingInterval: opts.startingInterval, + maxInterval: opts.maxInterval, + hiddenInterval: opts.hiddenInterval, + incrementByFactorOf: opts.incrementByFactorOf, + lazyStart: opts.lazyStart, + immediateExecution: opts.immediateExecution, + }; + + this.state = { + intervalId: null, + currentInterval: this.cfg.startingInterval, + pageVisibility: 'visible', + }; + + this.initInterval(); + } - state.intervalId = window.setInterval(() => { - cfg.callback(); + /* public */ - if (this.getCurrentInterval() === cfg.maxInterval) { - return; - } + start() { + const cfg = this.cfg; + const state = this.state; - this.incrementInterval(); - this.resume(); - }, this.getCurrentInterval()); + if (cfg.immediateExecution) { + cfg.immediateExecution = false; + cfg.callback(); } - // cancel the existing timer, setting the currentInterval back to startingInterval - cancel() { - this.setCurrentInterval(this.cfg.startingInterval); - this.stopTimer(); - } + state.intervalId = window.setInterval(() => { + cfg.callback(); - onVisibilityHidden() { - if (this.cfg.hiddenInterval) { - this.setCurrentInterval(this.cfg.hiddenInterval); - this.resume(); - } else { - this.cancel(); + if (this.getCurrentInterval() === cfg.maxInterval) { + return; } - } - // start a timer, using the existing interval - resume() { - this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped - this.start(); - } + this.incrementInterval(); + this.resume(); + }, this.getCurrentInterval()); + } - onVisibilityVisible() { - this.cancel(); - this.start(); - } + // cancel the existing timer, setting the currentInterval back to startingInterval + cancel() { + this.setCurrentInterval(this.cfg.startingInterval); + this.stopTimer(); + } - destroy() { + onVisibilityHidden() { + if (this.cfg.hiddenInterval) { + this.setCurrentInterval(this.cfg.hiddenInterval); + this.resume(); + } else { this.cancel(); - document.removeEventListener('visibilitychange', this.handleVisibilityChange); - $(document).off('visibilitychange').off('beforeunload'); } + } - /* private */ + // start a timer, using the existing interval + resume() { + this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped + this.start(); + } - initInterval() { - const cfg = this.cfg; + onVisibilityVisible() { + this.cancel(); + this.start(); + } - if (!cfg.lazyStart) { - this.start(); - } + destroy() { + this.cancel(); + document.removeEventListener('visibilitychange', this.handleVisibilityChange); + $(document).off('visibilitychange').off('beforeunload'); + } - this.initVisibilityChangeHandling(); - this.initPageUnloadHandling(); - } + /* private */ - initVisibilityChangeHandling() { - // cancel interval when tab no longer shown (prevents cached pages from polling) - document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); - } + initInterval() { + const cfg = this.cfg; - initPageUnloadHandling() { - // TODO: Consider refactoring in light of turbolinks removal. - // prevent interval continuing after page change, when kept in cache by Turbolinks - $(document).on('beforeunload', () => this.cancel()); + if (!cfg.lazyStart) { + this.start(); } - handleVisibilityChange(e) { - this.state.pageVisibility = e.target.visibilityState; - const intervalAction = this.isPageVisible() ? - this.onVisibilityVisible : - this.onVisibilityHidden; + this.initVisibilityChangeHandling(); + this.initPageUnloadHandling(); + } - intervalAction.apply(this); - } + initVisibilityChangeHandling() { + // cancel interval when tab no longer shown (prevents cached pages from polling) + document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); + } - getCurrentInterval() { - return this.state.currentInterval; - } + initPageUnloadHandling() { + // TODO: Consider refactoring in light of turbolinks removal. + // prevent interval continuing after page change, when kept in cache by Turbolinks + $(document).on('beforeunload', () => this.cancel()); + } - setCurrentInterval(newInterval) { - this.state.currentInterval = newInterval; - } + handleVisibilityChange(e) { + this.state.pageVisibility = e.target.visibilityState; + const intervalAction = this.isPageVisible() ? + this.onVisibilityVisible : + this.onVisibilityHidden; - incrementInterval() { - const cfg = this.cfg; - const currentInterval = this.getCurrentInterval(); - if (cfg.hiddenInterval && !this.isPageVisible()) return; - let nextInterval = currentInterval * cfg.incrementByFactorOf; + intervalAction.apply(this); + } - if (nextInterval > cfg.maxInterval) { - nextInterval = cfg.maxInterval; - } + getCurrentInterval() { + return this.state.currentInterval; + } + + setCurrentInterval(newInterval) { + this.state.currentInterval = newInterval; + } + + incrementInterval() { + const cfg = this.cfg; + const currentInterval = this.getCurrentInterval(); + if (cfg.hiddenInterval && !this.isPageVisible()) return; + let nextInterval = currentInterval * cfg.incrementByFactorOf; - this.setCurrentInterval(nextInterval); + if (nextInterval > cfg.maxInterval) { + nextInterval = cfg.maxInterval; } - isPageVisible() { return this.state.pageVisibility === 'visible'; } + this.setCurrentInterval(nextInterval); + } - stopTimer() { - const state = this.state; + isPageVisible() { return this.state.pageVisibility === 'visible'; } - state.intervalId = window.clearInterval(state.intervalId); - } + stopTimer() { + const state = this.state; + + state.intervalId = window.clearInterval(state.intervalId); } - gl.SmartInterval = SmartInterval; -})(window.gl || (window.gl = {})); +} + +window.gl.SmartInterval = SmartInterval; diff --git a/app/assets/javascripts/snippets_list.js b/app/assets/javascripts/snippets_list.js deleted file mode 100644 index 2128007113f..00000000000 --- a/app/assets/javascripts/snippets_list.js +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */ - -(global => { - global.gl = global.gl || {}; - - gl.SnippetsList = function() { - var $holder = $('.snippets-list-holder'); - - $holder.find('.pagination').on('ajax:success', (e, data) => { - $holder.replaceWith(data.html); - }); - }; -})(window); diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index c75b44cc2fd..3a06b477d7c 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,30 +1,28 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ /* global Flash */ -(function() { - this.Star = (function() { - function Star() { - $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { - var $starIcon, $starSpan, $this, toggleStar; - $this = $(this); - $starSpan = $this.find('span'); - $starIcon = $this.find('i'); - toggleStar = function(isStarred) { - $this.parent().find('.star-count').text(data.star_count); - if (isStarred) { - $starSpan.removeClass('starred').text('Star'); - $starIcon.removeClass('fa-star').addClass('fa-star-o'); - } else { - $starSpan.addClass('starred').text('Unstar'); - $starIcon.removeClass('fa-star-o').addClass('fa-star'); - } - }; - toggleStar($starSpan.hasClass('starred')); - }).on('ajax:error', function(e, xhr, status, error) { - new Flash('Star toggle failed. Try again later.', 'alert'); - }); - } +import { __, s__ } from './locale'; - return Star; - })(); -}).call(window); +export default class Star { + constructor() { + $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { + var $starIcon, $starSpan, $this, toggleStar; + $this = $(this); + $starSpan = $this.find('span'); + $starIcon = $this.find('i'); + toggleStar = function(isStarred) { + $this.parent().find('.star-count').text(data.star_count); + if (isStarred) { + $starSpan.removeClass('starred').text(s__('StarProject|Star')); + $starIcon.removeClass('fa-star').addClass('fa-star-o'); + } else { + $starSpan.addClass('starred').text(__('Unstar')); + $starIcon.removeClass('fa-star-o').addClass('fa-star'); + } + }; + toggleStar($starSpan.hasClass('starred')); + }).on('ajax:error', function(e, xhr, status, error) { + new Flash('Star toggle failed. Try again later.', 'alert'); + }); + } +} diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js index 5f9a3e00c22..bb4d68fcd49 100644 --- a/app/assets/javascripts/subscription.js +++ b/app/assets/javascripts/subscription.js @@ -1,47 +1,45 @@ -(() => { - class Subscription { - constructor(containerElm) { - this.containerElm = containerElm; +class Subscription { + constructor(containerElm) { + this.containerElm = containerElm; - const subscribeButton = containerElm.querySelector('.js-subscribe-button'); - if (subscribeButton) { - // remove class so we don't bind twice - subscribeButton.classList.remove('js-subscribe-button'); - subscribeButton.addEventListener('click', this.toggleSubscription.bind(this)); - } + const subscribeButton = containerElm.querySelector('.js-subscribe-button'); + if (subscribeButton) { + // remove class so we don't bind twice + subscribeButton.classList.remove('js-subscribe-button'); + subscribeButton.addEventListener('click', this.toggleSubscription.bind(this)); } + } - toggleSubscription(event) { - const button = event.currentTarget; - const buttonSpan = button.querySelector('span'); - if (!buttonSpan || button.classList.contains('disabled')) { - return; - } - button.classList.add('disabled'); + toggleSubscription(event) { + const button = event.currentTarget; + const buttonSpan = button.querySelector('span'); + if (!buttonSpan || button.classList.contains('disabled')) { + return; + } + button.classList.add('disabled'); - const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe'; - const toggleActionUrl = this.containerElm.dataset.url; + const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe'; + const toggleActionUrl = this.containerElm.dataset.url; - $.post(toggleActionUrl, () => { - button.classList.remove('disabled'); + $.post(toggleActionUrl, () => { + button.classList.remove('disabled'); - // hack to allow this to work with the issue boards Vue object - if (document.querySelector('html').classList.contains('issue-boards-page')) { - gl.issueBoards.boardStoreIssueSet( - 'subscribed', - !gl.issueBoards.BoardsStore.detail.issue.subscribed, - ); - } else { - buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe'; - } - }); - } + // hack to allow this to work with the issue boards Vue object + if (document.querySelector('html').classList.contains('issue-boards-page')) { + gl.issueBoards.boardStoreIssueSet( + 'subscribed', + !gl.issueBoards.BoardsStore.detail.issue.subscribed, + ); + } else { + buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe'; + } + }); + } - static bindAll(selector) { - [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm)); - } + static bindAll(selector) { + [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm)); } +} - window.gl = window.gl || {}; - window.gl.Subscription = Subscription; -})(); +window.gl = window.gl || {}; +window.gl.Subscription = Subscription; diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index 0cd591c7320..37e39ce5477 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,34 +1,33 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ -(function() { - this.SubscriptionSelect = (function() { - function SubscriptionSelect() { - $('.js-subscription-event').each(function(i, el) { - var fieldName; - fieldName = $(el).data("field-name"); - return $(el).glDropdown({ - selectable: true, - fieldName: fieldName, - toggleLabel: (function(_this) { - return function(selected, el, instance) { - var $item, label; - label = 'Subscription'; - $item = instance.dropdown.find('.is-active'); - if ($item.length) { - label = $item.text(); - } - return label; - }; - })(this), - clicked: function(options) { - return options.e.preventDefault(); - }, - id: function(obj, el) { - return $(el).data("id"); - } - }); + +class SubscriptionSelect { + constructor() { + $('.js-subscription-event').each(function(i, el) { + var fieldName; + fieldName = $(el).data("field-name"); + return $(el).glDropdown({ + selectable: true, + fieldName: fieldName, + toggleLabel: (function(_this) { + return function(selected, el, instance) { + var $item, label; + label = 'Subscription'; + $item = instance.dropdown.find('.is-active'); + if ($item.length) { + label = $item.text(); + } + return label; + }; + })(this), + clicked: function(options) { + return options.e.preventDefault(); + }, + id: function(obj, el) { + return $(el).data("id"); + } }); - } + }); + } +} - return SubscriptionSelect; - })(); -}).call(window); +window.SubscriptionSelect = SubscriptionSelect; diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index 7c063fae045..662d6b36c16 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -9,19 +9,18 @@ // // <div class="js-syntax-highlight"></div> // -(function() { - $.fn.syntaxHighlight = function() { - var $children; - if ($(this).hasClass('js-syntax-highlight')) { - // Given the element itself, apply highlighting - return $(this).addClass(gon.user_color_scheme); - } else { - // Given a parent element, recurse to any of its applicable children - $children = $(this).find('.js-syntax-highlight'); - if ($children.length) { - return $children.syntaxHighlight(); - } +$.fn.syntaxHighlight = function() { + var $children; + + if ($(this).hasClass('js-syntax-highlight')) { + // Given the element itself, apply highlighting + return $(this).addClass(gon.user_color_scheme); + } else { + // Given a parent element, recurse to any of its applicable children + $children = $(this).find('.js-syntax-highlight'); + if ($children.length) { + return $children.syntaxHighlight(); } - }; -}).call(window); + } +}; diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index 419c458ff34..c39f569da5e 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -2,7 +2,7 @@ import 'deckar01-task_list'; -class TaskList { +export default class TaskList { constructor(options = {}) { this.selector = options.selector; this.dataType = options.dataType; @@ -48,6 +48,3 @@ class TaskList { }); } } - -window.gl = window.gl || {}; -window.gl.TaskList = TaskList; diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index 7230946b484..bba8b5abbb4 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -2,7 +2,7 @@ import UsersSelect from './users_select'; -class Todos { +export default class Todos { constructor() { this.initFilters(); this.bindEvents(); @@ -37,10 +37,6 @@ class Todos { this.initFilterDropdown($('.js-type-search'), 'type'); this.initFilterDropdown($('.js-action-search'), 'action_id'); - $('form.filter-form').on('submit', function applyFilters(event) { - event.preventDefault(); - gl.utils.visitUrl(`${this.action}&${$(this).serialize()}`); - }); return new UsersSelect(); } @@ -159,6 +155,3 @@ class Todos { } } } - -window.gl = window.gl || {}; -gl.Todos = Todos; diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 76a821c7a17..7777ed1c3dc 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,68 +1,64 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */ -(function() { - this.TreeView = (function() { - function 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) { - var $clickedEl, path; - $clickedEl = $(e.target); - path = $('.tree-item-file-name a', this).attr('href'); - if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) { - if (e.metaKey || e.which === 2) { - e.preventDefault(); - return window.open(path, '_blank'); - } else { - return gl.utils.visitUrl(path); - } +export default class TreeView { + constructor() { + 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) { + var $clickedEl, path; + $clickedEl = $(e.target); + path = $('.tree-item-file-name a', this).attr('href'); + if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) { + if (e.metaKey || e.which === 2) { + e.preventDefault(); + return window.open(path, '_blank'); + } else { + return gl.utils.visitUrl(path); } - }); - // Show the "Loading commit data" for only the first element - $('span.log_loading:first').removeClass('hide'); - } + } + }); + // Show the "Loading commit data" for only the first element + $('span.log_loading:first').removeClass('hide'); + } - TreeView.prototype.initKeyNav = function() { - var li, liSelected; - 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)) { - return false; - } - if (e.which === 40) { - if (liSelected) { - next = liSelected.next(); - if (next.length > 0) { - liSelected.removeClass("selected"); - liSelected = next.addClass("selected"); - } - } else { - 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"); - } - } else { - liSelected = li.last().addClass("selected"); + initKeyNav() { + var li, liSelected; + 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)) { + return false; + } + if (e.which === 40) { + if (liSelected) { + next = liSelected.next(); + if (next.length > 0) { + liSelected.removeClass("selected"); + liSelected = next.addClass("selected"); } - return $(liSelected).focus(); - } else if (e.which === 13) { - path = $('.tree-item.selected .tree-item-file-name a').attr('href'); - if (path) { - return gl.utils.visitUrl(path); + } else { + 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"); } + } else { + liSelected = li.last().addClass("selected"); } - }); - }; - - return TreeView; - })(); -}).call(window); + return $(liSelected).focus(); + } else if (e.which === 13) { + path = $('.tree-item.selected .tree-item-file-name a').attr('href'); + if (path) { + return gl.utils.visitUrl(path); + } + } + }); + } +} diff --git a/app/assets/javascripts/usage_ping.js b/app/assets/javascripts/usage_ping.js index fd3af7d7ab6..2389056bd02 100644 --- a/app/assets/javascripts/usage_ping.js +++ b/app/assets/javascripts/usage_ping.js @@ -1,4 +1,4 @@ -function UsagePing() { +export default function UsagePing() { const usageDataUrl = $('.usage-data').data('endpoint'); $.ajax({ @@ -10,6 +10,3 @@ function UsagePing() { }, }); } - -window.gl = window.gl || {}; -window.gl.UsagePing = UsagePing; diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js deleted file mode 100644 index 19c9efe7fbd..00000000000 --- a/app/assets/javascripts/user.js +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */ - -import Cookies from 'js-cookie'; - -((global) => { - global.User = class { - constructor({ action }) { - this.action = action; - this.placeProfileAvatarsToTop(); - this.initTabs(); - this.hideProjectLimitMessage(); - } - - placeProfileAvatarsToTop() { - $('.profile-groups-avatars').tooltip({ - placement: 'top' - }); - } - - initTabs() { - return new global.UserTabs({ - parentEl: '.user-profile', - action: this.action - }); - } - - hideProjectLimitMessage() { - $('.hide-project-limit-message').on('click', e => { - e.preventDefault(); - Cookies.set('hide_project_limit_message', 'false'); - $(this).parents('.project-limit-message').remove(); - }); - } - }; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js deleted file mode 100644 index ce7eb76dc71..00000000000 --- a/app/assets/javascripts/user_tabs.js +++ /dev/null @@ -1,175 +0,0 @@ -/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, no-param-reassign, class-methods-use-this */ - -/* -UserTabs - -Handles persisting and restoring the current tab selection and lazily-loading -content on the Users#show page. - -### Example Markup - - <ul class="nav-links"> - <li class="activity-tab active"> - <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> - Activity - </a> - </li> - <li class="groups-tab"> - <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> - Groups - </a> - </li> - <li class="contributed-tab"> - <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed"> - Contributed projects - </a> - </li> - <li class="projects-tab"> - <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects"> - Personal projects - </a> - </li> - <li class="snippets-tab"> - <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets"> - </a> - </li> - </ul> - - <div class="tab-content"> - <div class="tab-pane" id="activity"> - Activity Content - </div> - <div class="tab-pane" id="groups"> - Groups Content - </div> - <div class="tab-pane" id="contributed"> - Contributed projects content - </div> - <div class="tab-pane" id="projects"> - Projects content - </div> - <div class="tab-pane" id="snippets"> - Snippets content - </div> - </div> - - <div class="loading-status"> - <div class="loading"> - Loading Animation - </div> - </div> -*/ -((global) => { - class UserTabs { - constructor ({ defaultAction, action, parentEl }) { - this.loaded = {}; - this.defaultAction = defaultAction || 'activity'; - this.action = action || this.defaultAction; - this.$parentEl = $(parentEl) || $(document); - this._location = window.location; - this.$parentEl.find('.nav-links a') - .each((i, navLink) => { - this.loaded[$(navLink).attr('data-action')] = false; - }); - this.actions = Object.keys(this.loaded); - this.bindEvents(); - - if (this.action === 'show') { - this.action = this.defaultAction; - } - - this.activateTab(this.action); - } - - bindEvents() { - this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this); - - this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') - .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)); - - this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper); - } - - changeProjectsPage(e) { - e.preventDefault(); - - $('.tab-pane.active').empty(); - const endpoint = $(e.target).attr('href'); - this.loadTab(this.getCurrentAction(), endpoint); - } - - tabShown(event) { - const $target = $(event.target); - const action = $target.data('action'); - const source = $target.attr('href'); - const endpoint = $target.data('endpoint'); - this.setTab(action, endpoint); - return this.setCurrentAction(source); - } - - activateTab(action) { - return this.$parentEl.find(`.nav-links .js-${action}-tab a`) - .tab('show'); - } - - setTab(action, endpoint) { - if (this.loaded[action]) { - return; - } - if (action === 'activity') { - this.loadActivities(); - } - - const loadableActions = ['groups', 'contributed', 'projects', 'snippets']; - if (loadableActions.indexOf(action) > -1) { - return this.loadTab(action, endpoint); - } - } - - loadTab(action, endpoint) { - return $.ajax({ - beforeSend: () => this.toggleLoading(true), - complete: () => this.toggleLoading(false), - dataType: 'json', - type: 'GET', - url: endpoint, - success: (data) => { - const tabSelector = `div#${action}`; - this.$parentEl.find(tabSelector).html(data.html); - this.loaded[action] = true; - return gl.utils.localTimeAgo($('.js-timeago', tabSelector)); - } - }); - } - - loadActivities() { - if (this.loaded['activity']) { - return; - } - const $calendarWrap = this.$parentEl.find('.user-calendar'); - $calendarWrap.load($calendarWrap.data('href')); - new gl.Activities(); - return this.loaded['activity'] = true; - } - - toggleLoading(status) { - return this.$parentEl.find('.loading-status .loading') - .toggle(status); - } - - setCurrentAction(source) { - let new_state = source; - new_state = new_state.replace(/\/+$/, ''); - new_state += this._location.search + this._location.hash; - history.replaceState({ - url: new_state - }, document.title, new_state); - return new_state; - } - - getCurrentAction() { - return this.$parentEl.find('.nav-links .active a').data('action'); - } - } - global.UserTabs = UserTabs; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/username_validator.js b/app/assets/javascripts/username_validator.js index 137cefa3b8e..a348d69153c 100644 --- a/app/assets/javascripts/username_validator.js +++ b/app/assets/javascripts/username_validator.js @@ -1,135 +1,131 @@ /* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ -((global) => { - const debounceTimeoutDuration = 1000; - const invalidInputClass = 'gl-field-error-outline'; - const successInputClass = 'gl-field-success-outline'; - const unavailableMessageSelector = '.username .validation-error'; - const successMessageSelector = '.username .validation-success'; - const pendingMessageSelector = '.username .validation-pending'; - const invalidMessageSelector = '.username .gl-field-error'; - - class UsernameValidator { - constructor() { - this.inputElement = $('#new_user_username'); - this.inputDomElement = this.inputElement.get(0); - this.state = { - available: false, - valid: false, - pending: false, - empty: true - }; - - const debounceTimeout = _.debounce((username) => { - this.validateUsername(username); - }, debounceTimeoutDuration); - - this.inputElement.on('keyup.username_check', () => { - const username = this.inputElement.val(); - - this.state.valid = this.inputDomElement.validity.valid; - this.state.empty = !username.length; - - if (this.state.valid) { - return debounceTimeout(username); - } - - this.renderState(); - }); - - // Override generic field validation - this.inputElement.on('invalid', this.interceptInvalid.bind(this)); - } - - renderState() { - // Clear all state - this.clearFieldValidationState(); +const debounceTimeoutDuration = 1000; +const invalidInputClass = 'gl-field-error-outline'; +const successInputClass = 'gl-field-success-outline'; +const unavailableMessageSelector = '.username .validation-error'; +const successMessageSelector = '.username .validation-success'; +const pendingMessageSelector = '.username .validation-pending'; +const invalidMessageSelector = '.username .gl-field-error'; + +export default class UsernameValidator { + constructor() { + this.inputElement = $('#new_user_username'); + this.inputDomElement = this.inputElement.get(0); + this.state = { + available: false, + valid: false, + pending: false, + empty: true + }; + + const debounceTimeout = _.debounce((username) => { + this.validateUsername(username); + }, debounceTimeoutDuration); + + this.inputElement.on('keyup.username_check', () => { + const username = this.inputElement.val(); + + this.state.valid = this.inputDomElement.validity.valid; + this.state.empty = !username.length; - if (this.state.valid && this.state.available) { - return this.setSuccessState(); + if (this.state.valid) { + return debounceTimeout(username); } - if (this.state.empty) { - return this.clearFieldValidationState(); - } + this.renderState(); + }); - if (this.state.pending) { - return this.setPendingState(); - } + // Override generic field validation + this.inputElement.on('invalid', this.interceptInvalid.bind(this)); + } - if (!this.state.available) { - return this.setUnavailableState(); - } + renderState() { + // Clear all state + this.clearFieldValidationState(); - if (!this.state.valid) { - return this.setInvalidState(); - } + if (this.state.valid && this.state.available) { + return this.setSuccessState(); } - interceptInvalid(event) { - event.preventDefault(); - event.stopPropagation(); + if (this.state.empty) { + return this.clearFieldValidationState(); } - validateUsername(username) { - if (this.state.valid) { - this.state.pending = true; - this.state.available = false; - this.renderState(); - return $.ajax({ - type: 'GET', - url: `${gon.relative_url_root}/users/${username}/exists`, - dataType: 'json', - success: (res) => this.setAvailabilityState(res.exists) - }); - } + if (this.state.pending) { + return this.setPendingState(); } - setAvailabilityState(usernameTaken) { - if (usernameTaken) { - this.state.valid = false; - this.state.available = false; - } else { - this.state.available = true; - } - this.state.pending = false; - this.renderState(); + if (!this.state.available) { + return this.setUnavailableState(); } - clearFieldValidationState() { - this.inputElement.siblings('p').hide(); - - this.inputElement.removeClass(invalidInputClass) - .removeClass(successInputClass); + if (!this.state.valid) { + return this.setInvalidState(); } + } - setUnavailableState() { - const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector); - this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); - $usernameUnavailableMessage.show(); - } + interceptInvalid(event) { + event.preventDefault(); + event.stopPropagation(); + } - setSuccessState() { - const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector); - this.inputElement.addClass(successInputClass).removeClass(invalidInputClass); - $usernameSuccessMessage.show(); + validateUsername(username) { + if (this.state.valid) { + this.state.pending = true; + this.state.available = false; + this.renderState(); + return $.ajax({ + type: 'GET', + url: `${gon.relative_url_root}/users/${username}/exists`, + dataType: 'json', + success: (res) => this.setAvailabilityState(res.exists) + }); } + } - setPendingState() { - const $usernamePendingMessage = $(pendingMessageSelector); - if (this.state.pending) { - $usernamePendingMessage.show(); - } else { - $usernamePendingMessage.hide(); - } + setAvailabilityState(usernameTaken) { + if (usernameTaken) { + this.state.valid = false; + this.state.available = false; + } else { + this.state.available = true; } + this.state.pending = false; + this.renderState(); + } - setInvalidState() { - const $inputErrorMessage = $(invalidMessageSelector); - this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); - $inputErrorMessage.show(); + clearFieldValidationState() { + this.inputElement.siblings('p').hide(); + + this.inputElement.removeClass(invalidInputClass) + .removeClass(successInputClass); + } + + setUnavailableState() { + const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector); + this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); + $usernameUnavailableMessage.show(); + } + + setSuccessState() { + const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector); + this.inputElement.addClass(successInputClass).removeClass(invalidInputClass); + $usernameSuccessMessage.show(); + } + + setPendingState() { + const $usernamePendingMessage = $(pendingMessageSelector); + if (this.state.pending) { + $usernamePendingMessage.show(); + } else { + $usernamePendingMessage.hide(); } } - global.UsernameValidator = UsernameValidator; -})(window); + setInvalidState() { + const $inputErrorMessage = $(invalidMessageSelector); + this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); + $inputErrorMessage.show(); + } +} diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js new file mode 100644 index 00000000000..f091e319f44 --- /dev/null +++ b/app/assets/javascripts/users/activity_calendar.js @@ -0,0 +1,224 @@ +import d3 from 'd3'; + +const LOADING_HTML = ` + <div class="text-center"> + <i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i> + </div> +`; + +function formatTooltipText({ date, count }) { + const dateObject = new Date(date); + const dateDayName = gl.utils.getDayName(dateObject); + const dateText = dateObject.format('mmm d, yyyy'); + + let contribText = 'No contributions'; + if (count > 0) { + contribText = `${count} contribution${count > 1 ? 's' : ''}`; + } + return `${contribText}<br />${dateDayName} ${dateText}`; +} + +const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]); + +export default class ActivityCalendar { + constructor(container, timestamps, calendarActivitiesPath) { + this.calendarActivitiesPath = calendarActivitiesPath; + this.clickDay = this.clickDay.bind(this); + this.currentSelectedDate = ''; + this.daySpace = 1; + this.daySize = 15; + this.daySizeWithSpace = this.daySize + (this.daySpace * 2); + this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + this.months = []; + + // Loop through the timestamps to create a group of objects + // The group of objects will be grouped based on the day of the week they are + this.timestampsTmp = []; + let group = 0; + + const today = new Date(); + today.setHours(0, 0, 0, 0, 0); + + const oneYearAgo = new Date(today); + oneYearAgo.setFullYear(today.getFullYear() - 1); + + const days = gl.utils.getDayDifference(oneYearAgo, today); + + for (let i = 0; i <= days; i += 1) { + const date = new Date(oneYearAgo); + date.setDate(date.getDate() + i); + + const day = date.getDay(); + const count = timestamps[date.format('yyyy-mm-dd')] || 0; + + // Create a new group array if this is the first day of the week + // or if is first object + if ((day === 0 && i !== 0) || i === 0) { + this.timestampsTmp.push([]); + group += 1; + } + + // Push to the inner array the values that will be used to render map + const innerArray = this.timestampsTmp[group - 1]; + innerArray.push({ count, date, day }); + } + + // Init color functions + this.colorKey = initColorKey(); + this.color = this.initColor(); + + // Init the svg element + this.svg = this.renderSvg(container, group); + this.renderDays(); + this.renderMonths(); + this.renderDayTitles(); + this.renderKey(); + + // Init tooltips + $(`${container} .js-tooltip`).tooltip({ html: true }); + } + + // Add extra padding for the last month label if it is also the last column + getExtraWidthPadding(group) { + let extraWidthPadding = 0; + const lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth(); + const secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth(); + + if (lastColMonth !== secondLastColMonth) { + extraWidthPadding = 3; + } + + return extraWidthPadding; + } + + renderSvg(container, group) { + const width = ((group + 1) * this.daySizeWithSpace) + this.getExtraWidthPadding(group); + return d3.select(container) + .append('svg') + .attr('width', width) + .attr('height', 167) + .attr('class', 'contrib-calendar'); + } + + renderDays() { + this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g') + .attr('transform', (group, i) => { + _.each(group, (stamp, a) => { + if (a === 0 && stamp.day === 0) { + const month = stamp.date.getMonth(); + const x = (this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace; + const lastMonth = _.last(this.months); + if ( + lastMonth == null || + (month !== lastMonth.month && x - this.daySizeWithSpace !== lastMonth.x) + ) { + this.months.push({ month, x }); + } + } + }); + return `translate(${(this.daySizeWithSpace * i) + 1 + this.daySizeWithSpace}, 18)`; + }) + .selectAll('rect') + .data(stamp => stamp) + .enter() + .append('rect') + .attr('x', '0') + .attr('y', stamp => this.daySizeWithSpace * stamp.day) + .attr('width', this.daySize) + .attr('height', this.daySize) + .attr('fill', stamp => ( + stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed' + )) + .attr('title', stamp => formatTooltipText(stamp)) + .attr('class', 'user-contrib-cell js-tooltip') + .attr('data-container', 'body') + .on('click', this.clickDay); + } + + renderDayTitles() { + const days = [ + { + text: 'M', + y: 29 + (this.daySizeWithSpace * 1), + }, { + text: 'W', + y: 29 + (this.daySizeWithSpace * 3), + }, { + text: 'F', + y: 29 + (this.daySizeWithSpace * 5), + }, + ]; + this.svg.append('g') + .selectAll('text') + .data(days) + .enter() + .append('text') + .attr('text-anchor', 'middle') + .attr('x', 8) + .attr('y', day => day.y) + .text(day => day.text) + .attr('class', 'user-contrib-text'); + } + + renderMonths() { + this.svg.append('g') + .attr('direction', 'ltr') + .selectAll('text') + .data(this.months) + .enter() + .append('text') + .attr('x', date => date.x) + .attr('y', 10) + .attr('class', 'user-contrib-text') + .text(date => this.monthNames[date.month]); + } + + renderKey() { + const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions']; + const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; + + this.svg.append('g') + .attr('transform', `translate(18, ${(this.daySizeWithSpace * 8) + 16})`) + .selectAll('rect') + .data(keyColors) + .enter() + .append('rect') + .attr('width', this.daySize) + .attr('height', this.daySize) + .attr('x', (color, i) => this.daySizeWithSpace * i) + .attr('y', 0) + .attr('fill', color => color) + .attr('class', 'js-tooltip') + .attr('title', (color, i) => keyValues[i]) + .attr('data-container', 'body'); + } + + initColor() { + const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; + return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange); + } + + clickDay(stamp) { + if (this.currentSelectedDate !== stamp.date) { + this.currentSelectedDate = stamp.date; + + const date = [ + this.currentSelectedDate.getFullYear(), + this.currentSelectedDate.getMonth() + 1, + this.currentSelectedDate.getDate(), + ].join('-'); + + $.ajax({ + url: this.calendarActivitiesPath, + data: { date }, + cache: false, + dataType: 'html', + beforeSend: () => $('.user-calendar-activities').html(LOADING_HTML), + success: data => $('.user-calendar-activities').html(data), + }); + } else { + this.currentSelectedDate = ''; + $('.user-calendar-activities').html(''); + } + } +} diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js deleted file mode 100644 index b11f691e424..00000000000 --- a/app/assets/javascripts/users/calendar.js +++ /dev/null @@ -1,231 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */ - -import d3 from 'd3'; - -(function() { - this.Calendar = (function() { - function Calendar(timestamps, calendar_activities_path) { - this.calendar_activities_path = calendar_activities_path; - this.clickDay = this.clickDay.bind(this); - this.currentSelectedDate = ''; - this.daySpace = 1; - this.daySize = 15; - this.daySizeWithSpace = this.daySize + (this.daySpace * 2); - this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; - this.months = []; - // Loop through the timestamps to create a group of objects - // The group of objects will be grouped based on the day of the week they are - this.timestampsTmp = []; - var group = 0; - - var today = new Date(); - today.setHours(0, 0, 0, 0, 0); - - var oneYearAgo = new Date(today); - oneYearAgo.setFullYear(today.getFullYear() - 1); - - var days = gl.utils.getDayDifference(oneYearAgo, today); - - for (var i = 0; i <= days; i += 1) { - var date = new Date(oneYearAgo); - date.setDate(date.getDate() + i); - - var day = date.getDay(); - var count = timestamps[date.format('yyyy-mm-dd')]; - - // Create a new group array if this is the first day of the week - // or if is first object - if ((day === 0 && i !== 0) || i === 0) { - this.timestampsTmp.push([]); - group += 1; - } - - var innerArray = this.timestampsTmp[group - 1]; - // Push to the inner array the values that will be used to render map - innerArray.push({ - count: count || 0, - date: date, - day: day - }); - } - - // Init color functions - this.colorKey = this.initColorKey(); - this.color = this.initColor(); - // Init the svg element - this.renderSvg(group); - this.renderDays(); - this.renderMonths(); - this.renderDayTitles(); - this.renderKey(); - this.initTooltips(); - } - - // Add extra padding for the last month label if it is also the last column - Calendar.prototype.getExtraWidthPadding = function(group) { - var extraWidthPadding = 0; - var lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth(); - var secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth(); - - if (lastColMonth != secondLastColMonth) { - extraWidthPadding = 3; - } - - return extraWidthPadding; - }; - - Calendar.prototype.renderSvg = function(group) { - var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group); - return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', width).attr('height', 167).attr('class', 'contrib-calendar'); - }; - - Calendar.prototype.renderDays = function() { - return this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g').attr('transform', (function(_this) { - return function(group, i) { - _.each(group, function(stamp, a) { - var lastMonth, lastMonthX, month, x; - if (a === 0 && stamp.day === 0) { - month = stamp.date.getMonth(); - x = (_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace; - lastMonth = _.last(_this.months); - if (lastMonth != null) { - lastMonthX = lastMonth.x; - } - if (lastMonth == null) { - return _this.months.push({ - month: month, - x: x - }); - } else if (month !== lastMonth.month && x - _this.daySizeWithSpace !== lastMonthX) { - return _this.months.push({ - month: month, - x: x - }); - } - } - }); - return "translate(" + ((_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace) + ", 18)"; - }; - })(this)).selectAll('rect').data(function(stamp) { - return stamp; - }).enter().append('rect').attr('x', '0').attr('y', (function(_this) { - return function(stamp, i) { - return _this.daySizeWithSpace * stamp.day; - }; - })(this)).attr('width', this.daySize).attr('height', this.daySize).attr('title', (function(_this) { - return function(stamp) { - var contribText, date, dateText; - date = new Date(stamp.date); - contribText = 'No contributions'; - if (stamp.count > 0) { - contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : ''); - } - dateText = date.format('mmm d, yyyy'); - return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText; - }; - })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) { - return function(stamp) { - if (stamp.count !== 0) { - return _this.color(Math.min(stamp.count, 40)); - } else { - return '#ededed'; - } - }; - })(this)).attr('data-container', 'body').on('click', this.clickDay); - }; - - Calendar.prototype.renderDayTitles = function() { - var days; - days = [ - { - text: 'M', - y: 29 + (this.daySizeWithSpace * 1) - }, { - text: 'W', - y: 29 + (this.daySizeWithSpace * 3) - }, { - text: 'F', - y: 29 + (this.daySizeWithSpace * 5) - } - ]; - return this.svg.append('g').selectAll('text').data(days).enter().append('text').attr('text-anchor', 'middle').attr('x', 8).attr('y', function(day) { - return day.y; - }).text(function(day) { - return day.text; - }).attr('class', 'user-contrib-text'); - }; - - Calendar.prototype.renderMonths = function() { - return this.svg.append('g').attr('direction', 'ltr').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) { - return date.x; - }).attr('y', 10).attr('class', 'user-contrib-text').text((function(_this) { - return function(date) { - return _this.monthNames[date.month]; - }; - })(this)); - }; - - Calendar.prototype.renderKey = function() { - const keyValues = ['no contributions', '1-9 contributions', '10-19 contributions', '20-29 contributions', '30+ contributions']; - const keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; - - this.svg.append('g') - .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`) - .selectAll('rect') - .data(keyColors) - .enter() - .append('rect') - .attr('width', this.daySize) - .attr('height', this.daySize) - .attr('x', (color, i) => this.daySizeWithSpace * i) - .attr('y', 0) - .attr('fill', color => color) - .attr('class', 'js-tooltip') - .attr('title', (color, i) => keyValues[i]) - .attr('data-container', 'body'); - }; - - Calendar.prototype.initColor = function() { - var colorRange; - colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)]; - return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange); - }; - - Calendar.prototype.initColorKey = function() { - return d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]); - }; - - Calendar.prototype.clickDay = function(stamp) { - var formatted_date; - if (this.currentSelectedDate !== stamp.date) { - this.currentSelectedDate = stamp.date; - formatted_date = this.currentSelectedDate.getFullYear() + "-" + (this.currentSelectedDate.getMonth() + 1) + "-" + this.currentSelectedDate.getDate(); - return $.ajax({ - url: this.calendar_activities_path, - data: { - date: formatted_date - }, - cache: false, - dataType: 'html', - beforeSend: function() { - return $('.user-calendar-activities').html('<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>'); - }, - success: function(data) { - return $('.user-calendar-activities').html(data); - } - }); - } else { - this.currentSelectedDate = ''; - return $('.user-calendar-activities').html(''); - } - }; - - Calendar.prototype.initTooltips = function() { - return $('.js-contrib-calendar .js-tooltip').tooltip({ - html: true - }); - }; - - return Calendar; - })(); -}).call(window); diff --git a/app/assets/javascripts/users/index.js b/app/assets/javascripts/users/index.js new file mode 100644 index 00000000000..33a83f8dae5 --- /dev/null +++ b/app/assets/javascripts/users/index.js @@ -0,0 +1,19 @@ +import Cookies from 'js-cookie'; +import UserTabs from './user_tabs'; + +export default function initUserProfile(action) { + // place profile avatars to top + $('.profile-groups-avatars').tooltip({ + placement: 'top', + }); + + // eslint-disable-next-line no-new + new UserTabs({ parentEl: '.user-profile', action }); + + // hide project limit message + $('.hide-project-limit-message').on('click', (e) => { + e.preventDefault(); + Cookies.set('hide_project_limit_message', 'false'); + $(this).parents('.project-limit-message').remove(); + }); +} diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js new file mode 100644 index 00000000000..5fe6603ce7b --- /dev/null +++ b/app/assets/javascripts/users/user_tabs.js @@ -0,0 +1,188 @@ +import ActivityCalendar from './activity_calendar'; + +/** + * UserTabs + * + * Handles persisting and restoring the current tab selection and lazily-loading + * content on the Users#show page. + * + * ### Example Markup + * + * <ul class="nav-links"> + * <li class="activity-tab active"> + * <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username"> + * Activity + * </a> + * </li> + * <li class="groups-tab"> + * <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups"> + * Groups + * </a> + * </li> + * <li class="contributed-tab"> + * ... + * </li> + * <li class="projects-tab"> + * ... + * </li> + * <li class="snippets-tab"> + * ... + * </li> + * </ul> + * + * <div class="tab-content"> + * <div class="tab-pane" id="activity"> + * Activity Content + * </div> + * <div class="tab-pane" id="groups"> + * Groups Content + * </div> + * <div class="tab-pane" id="contributed"> + * Contributed projects content + * </div> + * <div class="tab-pane" id="projects"> + * Projects content + * </div> + * <div class="tab-pane" id="snippets"> + * Snippets content + * </div> + * </div> + * + * <div class="loading-status"> + * <div class="loading"> + * Loading Animation + * </div> + * </div> + */ + +const CALENDAR_TEMPLATE = ` + <div class="clearfix calendar"> + <div class="js-contrib-calendar"></div> + <div class="calendar-hint"> + Summary of issues, merge requests, push events, and comments + </div> + </div> +`; + +export default class UserTabs { + constructor({ defaultAction, action, parentEl }) { + this.loaded = {}; + this.defaultAction = defaultAction || 'activity'; + this.action = action || this.defaultAction; + this.$parentEl = $(parentEl) || $(document); + this.windowLocation = window.location; + this.$parentEl.find('.nav-links a') + .each((i, navLink) => { + this.loaded[$(navLink).attr('data-action')] = false; + }); + this.actions = Object.keys(this.loaded); + this.bindEvents(); + + if (this.action === 'show') { + this.action = this.defaultAction; + } + + this.activateTab(this.action); + } + + bindEvents() { + this.$parentEl + .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') + .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)) + .on('click', '.gl-pagination a', event => this.changeProjectsPage(event)); + } + + changeProjectsPage(e) { + e.preventDefault(); + + $('.tab-pane.active').empty(); + const endpoint = $(e.target).attr('href'); + this.loadTab(this.getCurrentAction(), endpoint); + } + + tabShown(event) { + const $target = $(event.target); + const action = $target.data('action'); + const source = $target.attr('href'); + const endpoint = $target.data('endpoint'); + this.setTab(action, endpoint); + return this.setCurrentAction(source); + } + + activateTab(action) { + return this.$parentEl.find(`.nav-links .js-${action}-tab a`) + .tab('show'); + } + + setTab(action, endpoint) { + if (this.loaded[action]) { + return; + } + if (action === 'activity') { + this.loadActivities(); + } + + const loadableActions = ['groups', 'contributed', 'projects', 'snippets']; + if (loadableActions.indexOf(action) > -1) { + this.loadTab(action, endpoint); + } + } + + loadTab(action, endpoint) { + return $.ajax({ + beforeSend: () => this.toggleLoading(true), + complete: () => this.toggleLoading(false), + dataType: 'json', + url: endpoint, + success: (data) => { + const tabSelector = `div#${action}`; + this.$parentEl.find(tabSelector).html(data.html); + this.loaded[action] = true; + gl.utils.localTimeAgo($('.js-timeago', tabSelector)); + }, + }); + } + + loadActivities() { + if (this.loaded.activity) { + return; + } + const $calendarWrap = this.$parentEl.find('.user-calendar'); + const calendarPath = $calendarWrap.data('calendarPath'); + const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath'); + + $.ajax({ + dataType: 'json', + url: calendarPath, + success: (activityData) => { + $calendarWrap.html(CALENDAR_TEMPLATE); + + // eslint-disable-next-line no-new + new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath); + }, + }); + + // eslint-disable-next-line no-new + new gl.Activities(); + this.loaded.activity = true; + } + + toggleLoading(status) { + return this.$parentEl.find('.loading-status .loading') + .toggle(status); + } + + setCurrentAction(source) { + let newState = source; + newState = newState.replace(/\/+$/, ''); + newState += this.windowLocation.search + this.windowLocation.hash; + history.replaceState({ + url: newState, + }, document.title, newState); + return newState; + } + + getCurrentAction() { + return this.$parentEl.find('.nav-links .active a').data('action'); + } +} diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js deleted file mode 100644 index a38ce4eb25e..00000000000 --- a/app/assets/javascripts/users/users_bundle.js +++ /dev/null @@ -1 +0,0 @@ -import './calendar'; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index ec45253e50b..5728afb4c59 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) { return $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { - var isAuthorFilter; - isAuthorFilter = $('.js-author-search'); return _this.users(term, options, function(users) { // GitLabDropdownFilter returns this.instance // GitLabDropdownRemote returns this.options.instance @@ -643,7 +641,7 @@ 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 s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (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'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + ("@" + user.username || "") + "</div> </div>"; }; UsersSelect.prototype.formatSelection = function(user) { diff --git a/app/assets/javascripts/version_check_image.js b/app/assets/javascripts/version_check_image.js index 88ba991af47..ec515e892c6 100644 --- a/app/assets/javascripts/version_check_image.js +++ b/app/assets/javascripts/version_check_image.js @@ -3,6 +3,3 @@ export default class VersionCheckImage { imageElement.off('error').on('error', () => imageElement.hide()); } } - -window.gl = window.gl || {}; -gl.VersionCheckImage = VersionCheckImage; diff --git a/app/assets/javascripts/visibility_select.js b/app/assets/javascripts/visibility_select.js index f712d7ba930..0c928d0d5f6 100644 --- a/app/assets/javascripts/visibility_select.js +++ b/app/assets/javascripts/visibility_select.js @@ -1,27 +1,21 @@ -(() => { - const gl = window.gl || (window.gl = {}); - - class VisibilitySelect { - constructor(container) { - if (!container) throw new Error('VisibilitySelect requires a container element as argument 1'); - this.container = container; - this.helpBlock = this.container.querySelector('.help-block'); - this.select = this.container.querySelector('select'); - } - - init() { - if (this.select) { - this.updateHelpText(); - this.select.addEventListener('change', this.updateHelpText.bind(this)); - } else { - this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock; - } - } +export default class VisibilitySelect { + constructor(container) { + if (!container) throw new Error('VisibilitySelect requires a container element as argument 1'); + this.container = container; + this.helpBlock = this.container.querySelector('.help-block'); + this.select = this.container.querySelector('select'); + } - updateHelpText() { - this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description; + init() { + if (this.select) { + this.updateHelpText(); + this.select.addEventListener('change', this.updateHelpText.bind(this)); + } else { + this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock; } } - gl.VisibilitySelect = VisibilitySelect; -})(); + updateHelpText() { + this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description; + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index e8e22ad93a5..744a1cd24fa 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -108,7 +108,8 @@ export default { </div> <mr-widget-memory-usage v-if="deployment.metrics_url" - :metricsUrl="deployment.metrics_url" + :metrics-url="deployment.metrics_url" + :metrics-monitoring-url="deployment.metrics_monitoring_url" /> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js index f8b3fb748ae..8430548903c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js @@ -92,13 +92,13 @@ export default { :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}" :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''" data-placement="bottom"> - <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a> + <a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a> </span> </strong> <span v-if="shouldShowCommitsBehindText" class="diverged-commits-count"> - ({{mr.divergedCommitsCount}} {{commitsText}} behind) + (<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>) </span> </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js index 76cb71b6c12..534e2a88eff 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js @@ -7,7 +7,14 @@ import MRWidgetService from '../services/mr_widget_service'; export default { name: 'MemoryUsage', props: { - metricsUrl: { type: String, required: true }, + metricsUrl: { + type: String, + required: true, + }, + metricsMonitoringUrl: { + type: String, + required: true, + }, }, data() { return { @@ -124,7 +131,7 @@ export default { <p v-if="shouldShowMemoryGraph" class="usage-info js-usage-info"> - Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB + <a :href="metricsMonitoringUrl">Memory</a> usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB </p> <p v-if="shouldShowLoadFailure" diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 69bc1436284..fddafb0ddfa 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -48,6 +48,7 @@ export default class MergeRequestStore { this.sourceBranchLink = data.source_branch_with_namespace_link; this.mergeError = data.merge_error; this.targetBranchPath = data.target_branch_commits_path; + this.targetBranchTreePath = data.target_branch_tree_path; this.conflictResolutionPath = data.conflict_resolution_path; this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path; this.removeWIPPath = data.remove_wip_path; @@ -64,7 +65,7 @@ export default class MergeRequestStore { this.mergeCheckPath = data.merge_check_path; this.mergeActionsContentPath = data.commit_change_content_path; this.isRemovingSourceBranch = this.isRemovingSourceBranch || false; - this.isOpen = data.state === 'opened' || data.state === 'reopened' || false; + this.isOpen = data.state === 'opened'; this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false; this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false; this.canMerge = !!data.merge_path; 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 1d4d90f75b6..bdc059f4a03 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -2,7 +2,7 @@ import ciIconBadge from './ci_badge_link.vue'; import loadingIcon from './loading_icon.vue'; import timeagoTooltip from './time_ago_tooltip.vue'; -import tooltipMixin from '../mixins/tooltip'; +import tooltip from '../directives/tooltip'; import userAvatarImage from './user_avatar/user_avatar_image.vue'; /** @@ -47,9 +47,9 @@ export default { }, }, - mixins: [ - tooltipMixin, - ], + directives: { + tooltip, + }, components: { ciIconBadge, @@ -90,10 +90,10 @@ export default { <template v-if="user"> <a + v-tooltip :href="user.path" :title="user.email" - class="js-user-link commit-committer-link" - ref="tooltip"> + class="js-user-link commit-committer-link"> <user-avatar-image :img-src="user.avatar_url" diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue index 41b1d0165b0..15581d5c2a0 100644 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue @@ -12,9 +12,18 @@ required: false, default: '1', }, + + inline: { + type: Boolean, + required: false, + default: false, + }, }, computed: { + rootElementType() { + return this.inline ? 'span' : 'div'; + }, cssClass() { return `fa-${this.size}x`; }, @@ -22,12 +31,14 @@ }; </script> <template> - <div class="text-center"> + <component + :is="this.rootElementType" + class="text-center"> <i class="fa fa-spin fa-spinner" :class="cssClass" aria-hidden="true" :aria-label="label"> </i> - </div> + </component> </template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index e6977681e96..4e10bbc7408 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -44,9 +44,8 @@ text: this.$slots.textarea[0].elm.value, }, ) - .then((res) => { - const data = res.json(); - + .then(resp => resp.json()) + .then((data) => { this.markdownPreviewLoading = false; this.markdownPreview = data.body; @@ -64,6 +63,12 @@ */ return new gl.GLForm($(this.$refs['gl-form']), true); }, + beforeDestroy() { + const glForm = $(this.$refs['gl-form']).data('gl-form'); + if (glForm) { + glForm.destroy(); + } + }, }; </script> diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 1a11f493b7f..5bf2a90cc3b 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -1,17 +1,17 @@ <script> - import tooltipMixin from '../../mixins/tooltip'; + import tooltip from '../../directives/tooltip'; import toolbarButton from './toolbar_button.vue'; export default { - mixins: [ - tooltipMixin, - ], props: { previewMarkdown: { type: Boolean, required: true, }, }, + directives: { + tooltip, + }, components: { toolbarButton, }, @@ -94,13 +94,13 @@ </div> <div class="toolbar-group"> <button + v-tooltip aria-label="Go full screen" class="toolbar-btn js-zen-enter" data-container="body" tabindex="-1" title="Go full screen" - type="button" - ref="tooltip"> + type="button"> <i aria-hidden="true" class="fa fa-arrows-alt fa-fw"> 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 096be507625..f7da7ebfcfe 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -1,10 +1,7 @@ <script> - import tooltipMixin from '../../mixins/tooltip'; + import tooltip from '../../directives/tooltip'; export default { - mixins: [ - tooltipMixin, - ], props: { buttonTitle: { type: String, @@ -29,6 +26,9 @@ default: false, }, }, + directives: { + tooltip, + }, computed: { iconClass() { return `fa-${this.icon}`; @@ -39,10 +39,10 @@ <template> <button + v-tooltip type="button" class="toolbar-btn js-md hidden-xs" tabindex="-1" - ref="tooltip" data-container="body" :data-md-tag="tag" :data-md-block="tagBlock" diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 5e7df22dd83..c9dbc048345 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -46,6 +46,8 @@ export default { }, methods: { changePage(e) { + if (e.target.parentElement.classList.contains('disabled')) return; + const text = e.target.innerText; const { totalPages, nextPage, previousPage } = this.pageInfo; @@ -82,7 +84,9 @@ export default { const page = this.pageInfo.page; const items = []; - if (page > 1) items.push({ title: FIRST }); + if (page > 1) { + items.push({ title: FIRST, first: true }); + } if (page > 1) { items.push({ title: PREV, prev: true }); @@ -110,7 +114,9 @@ export default { items.push({ title: NEXT, next: true }); } - if (total - page >= 1) items.push({ title: LAST, last: true }); + if (total - page >= 1) { + items.push({ title: LAST, last: true }); + } return items; }, @@ -124,13 +130,15 @@ export default { v-for="item in getItems" :class="{ page: item.page, - prev: item.prev, - next: item.next, + 'js-previous-button': item.prev, + 'js-next-button': item.next, + 'js-last-button': item.last, + 'js-first-button': item.first, separator: item.separator, active: item.active, disabled: item.disabled }"> - <a @click="changePage($event)">{{item.title}}</a> + <a @click.prevent="changePage($event)">{{item.title}}</a> </li> </ul> </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 1c6ef071a6d..3ff7f6e2c4e 100644 --- a/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue +++ b/app/assets/javascripts/vue_shared/components/time_ago_tooltip.vue @@ -1,5 +1,5 @@ <script> -import tooltipMixin from '../mixins/tooltip'; +import tooltip from '../directives/tooltip'; import timeagoMixin from '../mixins/timeago'; import '../../lib/utils/datetime_utility'; @@ -28,19 +28,21 @@ export default { }, mixins: [ - tooltipMixin, timeagoMixin, ], + + directives: { + tooltip, + }, }; </script> <template> <time + v-tooltip :class="cssClass" - class="js-vue-timeago" :title="tooltipTitle(time)" :data-placement="tooltipPlacement" - data-container="body" - ref="tooltip"> + data-container="body"> {{timeFormated(time)}} </time> </template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index cd6f8c7aee4..dd9a2ebb184 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -16,11 +16,10 @@ */ import defaultAvatarUrl from 'images/no_avatar.png'; -import TooltipMixin from '../../mixins/tooltip'; +import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarImage', - mixins: [TooltipMixin], props: { imgSrc: { type: String, @@ -53,6 +52,9 @@ export default { default: 'top', }, }, + directives: { + tooltip, + }, computed: { tooltipContainer() { return this.tooltipText ? 'body' : null; @@ -72,6 +74,7 @@ export default { <template> <img + v-tooltip class="avatar" :class="[avatarSizeClass, cssClasses]" :src="imageSource" @@ -81,6 +84,5 @@ export default { :data-container="tooltipContainer" :data-placement="tooltipPlacement" :title="tooltipText" - ref="tooltip" /> </template> diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js new file mode 100644 index 00000000000..dc896cf5c7d --- /dev/null +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -0,0 +1,13 @@ +export default { + bind(el) { + $(el).tooltip(); + }, + + componentUpdated(el) { + $(el).tooltip('fixTitle'); + }, + + unbind(el) { + $(el).tooltip('destroy'); + }, +}; diff --git a/app/assets/javascripts/vue_shared/mixins/tooltip.js b/app/assets/javascripts/vue_shared/mixins/tooltip.js deleted file mode 100644 index 995c0c98505..00000000000 --- a/app/assets/javascripts/vue_shared/mixins/tooltip.js +++ /dev/null @@ -1,13 +0,0 @@ -export default { - mounted() { - $(this.$refs.tooltip).tooltip(); - }, - - updated() { - $(this.$refs.tooltip).tooltip('fixTitle'); - }, - - beforeDestroy() { - $(this.$refs.tooltip).tooltip('destroy'); - }, -}; diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index 740930dce5b..7f8e514fda1 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -14,11 +14,22 @@ Vue.http.interceptors.push((request, next) => { }); }); -// Inject CSRF token so we don't break any tests. +// Inject CSRF token and parse headers. +// New Vue Resource version uses Headers, we are expecting a plain object to render pagination +// and polling. Vue.http.interceptors.push((request, next) => { if ($.rails) { - // eslint-disable-next-line no-param-reassign - request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + request.headers.set('X-CSRF-Token', $.rails.csrfToken()); } - next(); + + next((response) => { + // Headers object has a `forEach` property that iterates through all values. + const headers = {}; + + response.headers.forEach((value, key) => { + headers[key] = value; + }); + // eslint-disable-next-line no-param-reassign + response.headers = headers; + }); }); diff --git a/app/assets/javascripts/webpack.js b/app/assets/javascripts/webpack.js new file mode 100644 index 00000000000..9a9cf395fb8 --- /dev/null +++ b/app/assets/javascripts/webpack.js @@ -0,0 +1,9 @@ +/** + * This is the first script loaded by webpack's runtime. It is used to manually configure + * config.output.publicPath to account for relative_url_root or CDN settings which cannot be + * baked-in to our webpack bundles. + */ + +if (gon && gon.webpack_public_path) { + __webpack_public_path__ = gon.webpack_public_path; // eslint-disable-line +} diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js index 4194c1bc08d..00676bcb0b3 100644 --- a/app/assets/javascripts/wikis.js +++ b/app/assets/javascripts/wikis.js @@ -1,69 +1,64 @@ -/* eslint-disable no-param-reassign */ /* global Breakpoints */ import 'vendor/jquery.nicescroll'; import './breakpoints'; -((global) => { - class Wikis { - constructor() { - this.bp = Breakpoints.get(); - this.sidebarEl = document.querySelector('.js-wiki-sidebar'); - this.sidebarExpanded = false; - $(this.sidebarEl).niceScroll(); +export default class Wikis { + constructor() { + this.bp = Breakpoints.get(); + this.sidebarEl = document.querySelector('.js-wiki-sidebar'); + this.sidebarExpanded = false; + $(this.sidebarEl).niceScroll(); - const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); - for (let i = 0; i < sidebarToggles.length; i += 1) { - sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); - } - - this.newWikiForm = document.querySelector('form.new-wiki-page'); - if (this.newWikiForm) { - this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e)); - } + const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); + for (let i = 0; i < sidebarToggles.length; i += 1) { + sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); + } - window.addEventListener('resize', () => this.renderSidebar()); - this.renderSidebar(); + this.newWikiForm = document.querySelector('form.new-wiki-page'); + if (this.newWikiForm) { + this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e)); } - handleNewWikiSubmit(e) { - if (!this.newWikiForm) return; + window.addEventListener('resize', () => this.renderSidebar()); + this.renderSidebar(); + } - const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); - const slug = gl.text.slugify(slugInput.value); + handleNewWikiSubmit(e) { + if (!this.newWikiForm) return; - if (slug.length > 0) { - const wikisPath = slugInput.getAttribute('data-wikis-path'); - window.location.href = `${wikisPath}/${slug}`; - e.preventDefault(); - } - } + const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); + const slug = gl.text.slugify(slugInput.value); - handleToggleSidebar(e) { + if (slug.length > 0) { + const wikisPath = slugInput.getAttribute('data-wikis-path'); + window.location.href = `${wikisPath}/${slug}`; e.preventDefault(); - this.sidebarExpanded = !this.sidebarExpanded; - this.renderSidebar(); } + } - sidebarCanCollapse() { - const bootstrapBreakpoint = this.bp.getBreakpointSize(); - return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; - } + handleToggleSidebar(e) { + e.preventDefault(); + this.sidebarExpanded = !this.sidebarExpanded; + this.renderSidebar(); + } - renderSidebar() { - if (!this.sidebarEl) return; - const { classList } = this.sidebarEl; - if (this.sidebarExpanded || !this.sidebarCanCollapse()) { - if (!classList.contains('right-sidebar-expanded')) { - classList.remove('right-sidebar-collapsed'); - classList.add('right-sidebar-expanded'); - } - } else if (classList.contains('right-sidebar-expanded')) { - classList.add('right-sidebar-collapsed'); - classList.remove('right-sidebar-expanded'); + sidebarCanCollapse() { + const bootstrapBreakpoint = this.bp.getBreakpointSize(); + return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; + } + + renderSidebar() { + if (!this.sidebarEl) return; + const { classList } = this.sidebarEl; + if (this.sidebarExpanded || !this.sidebarCanCollapse()) { + if (!classList.contains('right-sidebar-expanded')) { + classList.remove('right-sidebar-collapsed'); + classList.add('right-sidebar-expanded'); } + } else if (classList.contains('right-sidebar-expanded')) { + classList.add('right-sidebar-collapsed'); + classList.remove('right-sidebar-expanded'); } } - - global.Wikis = Wikis; -})(window.gl || (window.gl = {})); +} diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index b7fe552dec2..99c7644e4d9 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len, class-methods-use-this */ /* global Mousetrap */ // Zen Mode (full screen) textarea @@ -34,65 +34,62 @@ window.Dropzone = Dropzone; // **Cancelable** No // **Target** a.js-zen-leave // -(function() { - this.ZenMode = (function() { - function ZenMode() { - this.active_backdrop = null; - this.active_textarea = null; - $(document).on('click', '.js-zen-enter', function(e) { - e.preventDefault(); - return $(e.currentTarget).trigger('zen_mode:enter'); - }); - $(document).on('click', '.js-zen-leave', function(e) { - 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('keydown', function(e) { - // Esc - if (e.keyCode === 27) { - e.preventDefault(); - return $(document).trigger('zen_mode:leave'); - } - }); - } - - ZenMode.prototype.enter = function(backdrop) { - Mousetrap.pause(); - this.active_backdrop = $(backdrop); - this.active_backdrop.addClass('fullscreen'); - this.active_textarea = this.active_backdrop.find('textarea'); - // Prevent a user-resized textarea from persisting to fullscreen - this.active_textarea.removeAttr('style'); - return this.active_textarea.focus(); - }; - ZenMode.prototype.exit = function() { - if (this.active_textarea) { - Mousetrap.unpause(); - this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen'); - this.scrollTo(this.active_textarea); - this.active_textarea = null; - this.active_backdrop = null; - return Dropzone.forElement('.div-dropzone').enable(); +export default class ZenMode { + constructor() { + this.active_backdrop = null; + this.active_textarea = null; + $(document).on('click', '.js-zen-enter', function(e) { + e.preventDefault(); + return $(e.currentTarget).trigger('zen_mode:enter'); + }); + $(document).on('click', '.js-zen-leave', function(e) { + 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('keydown', function(e) { + // Esc + if (e.keyCode === 27) { + e.preventDefault(); + return $(document).trigger('zen_mode:leave'); } - }; + }); + } + + enter(backdrop) { + Mousetrap.pause(); + this.active_backdrop = $(backdrop); + this.active_backdrop.addClass('fullscreen'); + this.active_textarea = this.active_backdrop.find('textarea'); + // Prevent a user-resized textarea from persisting to fullscreen + this.active_textarea.removeAttr('style'); + return this.active_textarea.focus(); + } - ZenMode.prototype.scrollTo = function(zen_area) { - return $.scrollTo(zen_area, 0, { - offset: -150 - }); - }; + exit() { + if (this.active_textarea) { + Mousetrap.unpause(); + this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen'); + this.scrollTo(this.active_textarea); + this.active_textarea = null; + this.active_backdrop = null; + return Dropzone.forElement('.div-dropzone').enable(); + } + } - return ZenMode; - })(); -}).call(window); + scrollTo(zen_area) { + return $.scrollTo(zen_area, 0, { + offset: -150 + }); + } +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 83a8eeaafde..0665622fe4a 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -42,4 +42,4 @@ /* * Styles for JS behaviors. */ -@import "behaviors.scss"; +@import "behaviors"; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 9dc9f9a9068..6ce331a9129 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -4,49 +4,49 @@ @import 'framework/tw_bootstrap'; @import "framework/layout"; -@import "framework/animations.scss"; -@import "framework/avatar.scss"; -@import "framework/asciidoctor.scss"; -@import "framework/blocks.scss"; -@import "framework/buttons.scss"; -@import "framework/badges.scss"; -@import "framework/calendar.scss"; -@import "framework/callout.scss"; -@import "framework/common.scss"; -@import "framework/dropdowns.scss"; -@import "framework/files.scss"; -@import "framework/filters.scss"; -@import "framework/flash.scss"; -@import "framework/forms.scss"; -@import "framework/gfm.scss"; -@import "framework/header.scss"; -@import "framework/highlight.scss"; -@import "framework/issue_box.scss"; -@import "framework/jquery.scss"; -@import "framework/lists.scss"; -@import "framework/logo.scss"; -@import "framework/markdown_area.scss"; -@import "framework/mobile.scss"; -@import "framework/modal.scss"; -@import "framework/nav.scss"; -@import "framework/pagination.scss"; -@import "framework/panels.scss"; -@import "framework/selects.scss"; -@import "framework/sidebar.scss"; -@import "framework/tables.scss"; -@import "framework/notes.scss"; -@import "framework/timeline.scss"; -@import "framework/typography.scss"; -@import "framework/zen.scss"; +@import "framework/animations"; +@import "framework/avatar"; +@import "framework/asciidoctor"; +@import "framework/blocks"; +@import "framework/buttons"; +@import "framework/badges"; +@import "framework/calendar"; +@import "framework/callout"; +@import "framework/common"; +@import "framework/dropdowns"; +@import "framework/files"; +@import "framework/filters"; +@import "framework/flash"; +@import "framework/forms"; +@import "framework/gfm"; +@import "framework/header"; +@import "framework/highlight"; +@import "framework/issue_box"; +@import "framework/jquery"; +@import "framework/lists"; +@import "framework/logo"; +@import "framework/markdown_area"; +@import "framework/mobile"; +@import "framework/modal"; +@import "framework/nav"; +@import "framework/pagination"; +@import "framework/panels"; +@import "framework/selects"; +@import "framework/sidebar"; +@import "framework/tables"; +@import "framework/notes"; +@import "framework/timeline"; +@import "framework/typography"; +@import "framework/zen"; @import "framework/blank"; -@import "framework/wells.scss"; -@import "framework/page-header.scss"; -@import "framework/awards.scss"; -@import "framework/images.scss"; +@import "framework/wells"; +@import "framework/page-header"; +@import "framework/awards"; +@import "framework/images"; @import "framework/broadcast-messages"; -@import "framework/emojis.scss"; -@import "framework/emoji-sprites.scss"; -@import "framework/icons.scss"; -@import "framework/snippets.scss"; -@import "framework/memory_graph.scss"; -@import "framework/responsive-tables.scss"; +@import "framework/emojis"; +@import "framework/emoji-sprites"; +@import "framework/icons"; +@import "framework/snippets"; +@import "framework/memory_graph"; +@import "framework/responsive-tables"; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 4ae2b164d2e..cb41df8a88d 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -35,6 +35,8 @@ width: 40px; height: 40px; padding: 0; + background: $avatar-background; + overflow: hidden; &.avatar-inline { float: none; @@ -60,7 +62,7 @@ } &:not([href]):hover { - border-color: rgba($avatar-border, .2); + border-color: darken($avatar-border, 10%); } } @@ -86,6 +88,10 @@ overflow: hidden; display: flex; + a { + display: flex; + } + .avatar { border-radius: 0; border: none; @@ -99,7 +105,7 @@ .avatar-counter { background-color: $gray-darkest; color: $white-light; - border: 1px solid $border-color; + border: 1px solid $avatar-border; border-radius: 1em; font-family: $regular_font; font-size: 9px; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 19166757e64..bb30da4f4b2 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -24,7 +24,7 @@ opacity: 0; transform: scale(.2); transform-origin: 0 -45px; - transition: .3s cubic-bezier(.67,.06,.19,1.44); + transition: .3s cubic-bezier(.67, .06, .19, 1.44); transition-property: transform, opacity; &.is-aligned-right { @@ -231,11 +231,11 @@ .award-control-icon-positive, .award-control-icon-super-positive { + @include transition(opacity, transform); position: absolute; left: 10px; bottom: 6px; opacity: 0; - @include transition(opacity, transform); } .award-control-text { diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index a2fa2e7769b..6bb096fc5bd 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -1,9 +1,14 @@ -.blank-state-welcome { - text-align: center; - border-bottom: 1px solid $border-color; +.blank-state-parent-container { + .section-container { + padding: 10px; + } - .blank-state-text { - margin-bottom: 0; + .section-body { + width: 100%; + height: 100%; + padding-bottom: 25px; + border: 1px solid $border-color; + border-radius: $border-radius-default; } } @@ -11,41 +16,35 @@ padding-top: 20px; padding-bottom: 20px; text-align: center; -} -.blank-state-no-icon { - padding-top: 40px; - padding-bottom: 40px; -} - -.blank-state-icon { - padding-bottom: 20px; - color: $gray-darkest; - font-size: 56px; + &.blank-state-welcome { + .blank-state-welcome-title { + font-size: 24px; + } - path, - polygon { - fill: currentColor; + .blank-state-text { + margin-bottom: 0; + } } -} -.blank-state-title { - margin-top: 0; - margin-bottom: 5px; - font-size: 18px; - font-weight: normal; -} + .blank-state-icon { + padding-bottom: 20px; -.blank-state-text { - margin-top: 0; - margin-bottom: $gl-padding; - font-size: 14px; + svg { + display: block; + margin: auto; + } + } - > strong { - font-weight: 600; + .blank-state-title { + margin-top: 0; + margin-bottom: 10px; + font-size: 18px; } -} -.blank-state-welcome-title { - font-size: 24px; + .blank-state-text { + max-width: $container-text-max-width; + margin: 0 auto $gl-padding; + font-size: 14px; + } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 4369ae78bde..6eabdc63d9e 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -20,17 +20,29 @@ color: $text; border-color: $border; + > .icon { + color: $text; + } + &:hover, &:focus { background-color: $hover-background; border-color: $hover-border; color: $hover-text; + + > .icon { + color: $hover-text; + } } &:active { background-color: $active-background; border-color: $active-border; color: $hover-text; + + > .icon { + color: $hover-text; + } } } @@ -163,7 +175,8 @@ @include btn-orange; } - &.btn-close { + &.btn-close, + &.btn-close-color { @include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700); } @@ -181,7 +194,8 @@ float: right; } - &.btn-reopen { + &.btn-reopen, + .btn-reopen-color { /* should be same as parent class for now */ } diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 759401a7806..0ac095f7d8f 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -93,7 +93,7 @@ .is-selected .pika-day, .pika-day:hover, - .is-today .pika-day:hover { + .is-today .pika-day { background: $gl-primary; color: $white-light; box-shadow: none; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 00c981f64c5..5e374360359 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -336,11 +336,6 @@ table { text-align: center; } -#nprogress .spinner { - top: 15px !important; - right: 10px !important; -} - .header-with-avatar { h3 { margin: 0; @@ -450,4 +445,3 @@ table { pointer-events: none; opacity: .5; } - diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cba890ce831..3f934403147 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -35,8 +35,8 @@ .open { .dropdown-menu, .dropdown-menu-nav { - display: block; @include set-visible; + display: block; @media (max-width: $screen-xs-max) { width: 100%; @@ -148,7 +148,6 @@ padding: 5px 8px; color: $gl-text-color; line-height: initial; - text-overflow: ellipsis; border-radius: 2px; white-space: nowrap; overflow: hidden; @@ -184,13 +183,15 @@ .dropdown-menu, .dropdown-menu-nav { + @include set-invisible; display: block; position: absolute; - width: 100%; + width: auto; top: 100%; left: 0; z-index: 9; min-width: 240px; + max-width: 500px; margin-top: 2px; margin-bottom: 0; font-size: 14px; @@ -200,11 +201,6 @@ border: 1px solid $dropdown-border-color; border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; - @include set-invisible; - - @media (max-width: $screen-sm-min) { - width: 100%; - } &.dropdown-open-left { right: 0; @@ -288,16 +284,84 @@ color: $gl-text-color-secondary; } - .badge { - position: absolute; - right: 8px; - top: 5px; + .badge + span:not(.badge) { + // Expects up to 3 digits on the badge + margin-right: 40px; } } -.filtered-search-box-input-container .dropdown-menu, -.filtered-search-box-input-container .dropdown-menu-nav, -.comment-type-dropdown .dropdown-menu { +.droplab-dropdown { + .dropdown-toggle > i { + pointer-events: none; + } + + .dropdown-menu li { + padding: $gl-btn-padding; + cursor: pointer; + + > a, + > button { + display: flex; + margin: 0; + padding: 0; + border-radius: 0; + text-overflow: inherit; + background-color: inherit; + color: inherit; + border: inherit; + text-align: left; + + &:hover, + &:focus { + background-color: inherit; + color: inherit; + } + + &.btn .fa:not(:last-child) { + margin-left: 5px; + } + } + + &:hover, + &:focus { + background-color: $dropdown-hover-color; + color: $white-light; + } + + &.droplab-item-selected i { + visibility: visible; + } + + &.divider { + margin: 0 8px; + padding: 0; + border-top: $gray-darkest; + } + + .icon { + visibility: hidden; + } + + .description { + display: inline-block; + white-space: normal; + margin-left: 5px; + + p { + margin-bottom: 0; + } + } + } + + .icon { + display: inline-block; + vertical-align: top; + padding-top: 2px; + } +} + +.droplab-dropdown .dropdown-menu, +.droplab-dropdown .dropdown-menu-nav { display: none; opacity: 1; visibility: visible; @@ -308,7 +372,6 @@ .dropdown-menu, .dropdown-menu-nav { max-width: 280px; - width: auto; } } @@ -395,6 +458,7 @@ .dropdown-menu-align-right { left: auto; right: 0; + margin-top: -5px; } .dropdown-menu-selectable { @@ -605,8 +669,8 @@ } .pika-single { - position: relative!important; - top: 0!important; + position: relative !important; + top: 0 !important; border: 0; box-shadow: none; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index da03e4f5b5e..8ad082f7a65 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -12,6 +12,14 @@ &.readme-holder { margin: $gl-padding 0; + + &.limited-width-container .file-content { + max-width: $limited-layout-width-sm; + margin-left: auto; + margin-right: auto; + padding-top: 64px; + padding-bottom: 64px; + } } table { @@ -123,7 +131,7 @@ } /** - * Annotate file + * Blame file */ &.blame { table { @@ -155,8 +163,18 @@ td.blame-commit { padding: 5px 10px; min-width: 400px; + max-width: 400px; background: $gray-light; border-left: 3px solid; + + .commit-row-title { + display: flex; + } + + .item-title { + flex: 1; + margin-right: 0.5em; + } } @for $i from 0 through 5 { diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 767cf5ffea5..ec13a86ccf7 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -70,6 +70,13 @@ .input-token { max-width: 200px; + padding: 0; + + &:hover, + &:focus { + background-color: inherit; + color: inherit; + } } .input-token:only-child, @@ -156,6 +163,16 @@ } } +.droplab-dropdown li.filtered-search-token { + padding: 0; + + &:hover, + &:focus { + background-color: inherit; + color: inherit; + } +} + .filtered-search-term { .name { background-color: inherit; @@ -258,7 +275,7 @@ } .filtered-search-input-dropdown-menu { - max-height: 215px; + max-height: 225px; max-width: 280px; overflow: auto; @@ -351,7 +368,7 @@ margin-right: 0.3em; } - & > .value { + > .value { font-weight: 600; } } @@ -365,10 +382,6 @@ } } -.dropdown-menu .filter-dropdown-item { - padding: 0; -} - @media (max-width: $screen-xs-max) { .issues-details-filters { padding: 0 0 10px; @@ -380,7 +393,8 @@ @media (max-width: $screen-xs) { .filter-dropdown-container { .dropdown-toggle, - .dropdown { + .dropdown, + .dropdown-menu { width: 100%; } @@ -400,13 +414,16 @@ background-color: $dropdown-hover-color; color: $white-light; text-decoration: none; + outline: 0; .avatar { border-color: $white-light; } } -.filter-dropdown-item { +.droplab-dropdown .dropdown-menu .filter-dropdown-item { + padding: 0; + .btn { border: none; width: 100%; @@ -418,6 +435,7 @@ .fa { width: 15px; + line-height: $line-height-base; } .dropdown-label-box { @@ -440,17 +458,14 @@ } .dropdown-user { - display: -webkit-flex; display: flex; } .dropdown-user-details { - display: -webkit-flex; display: flex; - -webkit-flex-direction: column; flex-direction: column; - &> span { + > span { white-space: normal; word-break: break-all; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index d8645afb7da..1c4238bc564 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -34,6 +34,8 @@ header { top: 0; left: 0; right: 0; + color: $gl-text-color-secondary; + border-radius: 0; @media (max-width: $screen-xs-min) { padding: 0 16px; @@ -59,7 +61,7 @@ header { padding: 0; .nav > li > a { - color: $gl-text-color-secondary; + color: currentColor; font-size: 18px; padding: 0; margin: (($header-height - 28) / 2) 3px; @@ -84,7 +86,7 @@ header { &:hover, &:focus, &:active { - background-color: $gray-light; + background-color: transparent; color: $gl-text-color; svg { @@ -96,13 +98,19 @@ header { font-size: 14px; } + .fa-chevron-down { + position: relative; + top: -3px; + font-size: 10px; + } + svg { position: relative; top: 2px; height: 17px; // hack to get SVG to line up with FA icons width: 23px; - fill: $gl-text-color-secondary; + fill: currentColor; } } @@ -124,6 +132,22 @@ header { } } + &.navbar-gitlab-new { + .fa-times { + display: none; + } + + .menu-expanded { + .fa-ellipsis-v { + display: none; + } + + .fa-times { + display: block; + } + } + } + .global-dropdown { position: absolute; left: -10px; @@ -163,6 +187,19 @@ header { min-height: $header-height; padding-left: 30px; + &.menu-expanded { + @media (max-width: $screen-xs-max) { + .header-logo, + .title-container { + display: none; + } + + .navbar-collapse { + display: block; + } + } + } + .dropdown-menu { margin-top: -5px; } @@ -225,7 +262,7 @@ header { } a { - color: $gl-text-color; + color: currentColor; &:hover { text-decoration: underline; @@ -276,6 +313,29 @@ header { .impersonation i { color: $red-500; } + + // TODO: fallback to global style + .dropdown-menu, + .dropdown-menu-nav { + li { + padding: 0 1px; + + a { + border-radius: 0; + padding: 8px 16px; + + &:hover, + &:active, + &:focus { + background-color: $gray-darker; + } + } + } + } +} + +.with-performance-bar header.navbar-gitlab { + top: $performance-bar-height; } .navbar-nav { @@ -322,7 +382,7 @@ header { padding-left: 5px; .nav > li:not(.hidden-xs) { - display: table-cell!important; + display: table-cell !important; width: 25%; a { @@ -346,6 +406,8 @@ header { width: auto; min-width: 140px; margin-top: -5px; + color: $gl-text-color; + left: auto; .current-user { padding: 5px 18px; diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 6d27d7568cf..71d5949b023 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -61,7 +61,7 @@ &:focus { outline: none; - & i { + i { visibility: visible; } } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 49bff23452d..67c3287ed74 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -53,7 +53,7 @@ body { } &.limit-container-width-sm { - max-width: 790px; + max-width: $limited-layout-width-sm; } } @@ -120,3 +120,7 @@ of the body element here, we negate cascading side effects but allow momentum sc .page-with-sidebar { -webkit-overflow-scrolling: auto; } + +.with-performance-bar .page-with-sidebar { + margin-top: $header-height + $performance-bar-height; +} diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 38727e15c6f..868e65a8f46 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -236,6 +236,8 @@ ul.content-list { ul.controls { float: right; list-style: none; + display: flex; + align-items: center; .btn { padding: 10px 14px; @@ -259,6 +261,12 @@ ul.controls { } } } + + .issuable-pipeline-broken a, + .issuable-pipeline-status a, + .author_link { + display: flex; + } } ul.indent-list { @@ -343,6 +351,12 @@ ul.indent-list { .group-row { padding: 0; border: none; + + &:last-of-type { + .group-row-contents:not(:hover) { + border-bottom: 1px solid transparent; + } + } } .group-row-contents { diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index b21bcc22a87..a2de4598167 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -165,8 +165,8 @@ .cur { .avatar { - border: 1px solid $white-light; @include disableAllAnimation; + border: 1px solid $white-light; } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 3a98332e46c..6f91d11b369 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -118,3 +118,29 @@ @content; } } + +/* + * Mixin for status badges, as used for pipelines and commit signatures + */ +@mixin status-color($color-light, $color-main, $color-dark) { + color: $color-main; + border-color: $color-main; + + &:not(span):hover { + background-color: $color-light; + color: $color-dark; + border-color: $color-dark; + + svg { + fill: $color-dark; + } + } + + svg { + fill: $color-main; + } +} + +@mixin green-status-color { + @include status-color($green-50, $green-500, $green-700); +} diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 7098203321d..a28f54936be 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -21,3 +21,9 @@ body.modal-open { width: 860px; } } + +@media (min-width: $screen-lg-min) { + .modal-full { + width: 98%; + } +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 3787ef370b2..88e7ba117d5 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -45,8 +45,7 @@ li { display: flex; - a, - .btn-link { + a { padding: $gl-btn-padding; padding-bottom: 11px; font-size: 14px; @@ -68,29 +67,7 @@ } } - .btn-link { - padding-top: 16px; - padding-left: 15px; - padding-right: 15px; - border-left: none; - border-right: none; - border-top: none; - border-radius: 0; - - &:hover, - &:active, - &:focus { - background-color: transparent; - } - - &:active { - outline: 0; - box-shadow: none; - } - } - - &.active a, - &.active .btn-link { + &.active a { border-bottom: 2px solid $link-underline-blue; color: $black; font-weight: 600; @@ -205,6 +182,12 @@ } } + &.nav-controls-new-nav { + > .dropdown { + margin-right: 0; + } + } + > .btn-grouped { float: none; } @@ -213,14 +196,6 @@ display: none; } - .btn, - .dropdown, - .dropdown-toggle, - input, - form { - height: 35px; - } - input { display: inline-block; position: relative; @@ -348,7 +323,7 @@ position: absolute; top: 7px; right: 15px; - z-index: 2; + z-index: 300; li.active { font-weight: bold; @@ -372,6 +347,10 @@ } } +.with-performance-bar .layout-nav { + margin-top: $header-height + $performance-bar-height; +} + .scrolling-tabs-container { position: relative; @@ -466,6 +445,22 @@ } } +.with-performance-bar .page-with-layout-nav { + .right-sidebar { + top: ($header-height + 1) * 2 + $performance-bar-height; + } + + &.page-with-sub-nav { + .right-sidebar { + top: ($header-height + 1) * 3 + $performance-bar-height; + + &.affix { + top: $header-height + $performance-bar-height; + } + } + } +} + .nav-block { &.activities { border-bottom: 1px solid $border-color; diff --git a/app/assets/stylesheets/framework/responsive-tables.scss b/app/assets/stylesheets/framework/responsive-tables.scss index d2c90908baa..8e653c443cf 100644 --- a/app/assets/stylesheets/framework/responsive-tables.scss +++ b/app/assets/stylesheets/framework/responsive-tables.scss @@ -100,9 +100,9 @@ } .table-mobile-header { + @include flex-max-width(40); color: $gl-text-color-secondary; text-align: left; - @include flex-max-width(40); @media (min-width: $screen-md-min) { display: none; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 5cf9330b8f8..09b60ad1676 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -89,10 +89,13 @@ } } +.with-performance-bar .right-sidebar.affix { + top: $header-height + $performance-bar-height; +} + @mixin maintain-sidebar-dimensions { display: block; width: $gutter-width; - padding: 10px 20px; } .issues-bulk-update.right-sidebar { @@ -104,6 +107,15 @@ &.right-sidebar-expanded { @include maintain-sidebar-dimensions; width: $gutter-width; + + .issuable-sidebar-header { + // matches `.top-area .nav-controls` for issuable index pages + padding: 11px 0; + } + + .block:last-of-type { + border: none; + } } &.right-sidebar-collapsed { diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 10881987038..3d68a50f91f 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -44,6 +44,10 @@ &:target, &.target { background: $line-target-blue; + + &.system-note .note-body .note-text.system-note-commit-list::after { + background: linear-gradient(rgba($line-target-blue, 0.1) -100px, $line-target-blue 100%); + } } .avatar { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 785b09e622f..bf5f124d142 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -2,13 +2,26 @@ color: $gl-text-color; word-wrap: break-word; + [dir="auto"] { + text-align: initial; + } + a { color: $md-link-color; } img { - max-width: 100%; + /*max-width: 100%;*/ margin: 0 0 8px; + min-width: 200px; + min-height: 100px; + background-color: $gray-lightest; + } + + img.js-lazy-loaded { + min-width: inherit; + min-height: inherit; + background-color: inherit; } p a:not(.no-attachment-icon) img { @@ -112,9 +125,12 @@ blockquote p { color: $gl-grayish-blue !important; - margin: 0; font-size: inherit; line-height: 1.5; + + &:last-child { + margin: 0; + } } p { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 71513199d84..0df6f24bfe6 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -74,6 +74,18 @@ $red-700: #a62d19; $red-800: #8b2615; $red-900: #711e11; +$indigo-50: #f7f7ff; +$indigo-100: #ebebfa; +$indigo-200: #d1d1f0; +$indigo-300: #a6a6de; +$indigo-400: #7c7ccc; +$indigo-500: #6666c4; +$indigo-600: #5b5bbd; +$indigo-700: #4b4ba3; +$indigo-800: #393982; +$indigo-900: #292961; +$indigo-950: #1a1a40; + $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); @@ -98,9 +110,10 @@ $well-light-text-color: #5b6169; * Text */ $gl-font-size: 14px; -$gl-text-color: rgba(0, 0, 0, .85); -$gl-text-color-secondary: rgba(0, 0, 0, .55); -$gl-text-color-disabled: rgba(0, 0, 0, .35); +$gl-text-color: #2e2e2e; +$gl-text-color-secondary: #707070; +$gl-text-color-tertiary: #949494; +$gl-text-color-quaternary: #d6d6d6; $gl-text-color-inverted: rgba(255, 255, 255, 1.0); $gl-text-color-secondary-inverted: rgba(255, 255, 255, .85); $gl-text-green: $green-600; @@ -114,7 +127,7 @@ $gl-gray-dark: #313236; $gl-gray-light: #5c5c5c; $gl-header-color: #4c4e54; $gl-header-nav-hover-color: #434343; -$placeholder-text-color: rgba(0, 0, 0, .42); +$placeholder-text-color: $gl-text-color-tertiary; /* * Lists @@ -122,7 +135,7 @@ $placeholder-text-color: rgba(0, 0, 0, .42); $list-font-size: $gl-font-size; $list-title-color: $gl-text-color; $list-text-color: $gl-text-color; -$list-text-disabled-color: $gl-text-color-disabled; +$list-text-disabled-color: $gl-text-color-tertiary; $list-border-light: #eee; $list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; @@ -146,6 +159,7 @@ $code_line_height: 1.6; * Padding */ $gl-padding: 16px; +$gl-col-padding: 15px; $gl-btn-padding: 10px; $gl-input-padding: 10px; $gl-vert-padding: 6px; @@ -161,6 +175,8 @@ $progress-color: #c0392b; $header-height: 50px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; +$limited-layout-width-sm: 790px; +$container-text-max-width: 540px; $gl-avatar-size: 40px; $error-exclamation-point: $red-500; $border-radius-default: 3px; @@ -188,6 +204,7 @@ $divergence-graph-separator-bg: #ccc; $general-hover-transition-duration: 100ms; $general-hover-transition-curve: linear; $highlight-changes-color: rgb(235, 255, 232); +$performance-bar-height: 35px; /* @@ -256,7 +273,7 @@ $diff-view-modes-border: #c1c1c1; /* * Fonts */ -$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; +$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; /* @@ -301,7 +318,7 @@ $badge-color: $gl-text-color-secondary; /* * Award emoji */ -$award-emoji-menu-shadow: rgba(0,0,0,.175); +$award-emoji-menu-shadow: rgba(0, 0, 0, .175); $award-emoji-positive-add-bg: #fed159; $award-emoji-positive-add-lines: #bb9c13; @@ -323,6 +340,7 @@ $note-disabled-comment-color: #b2b2b2; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; $note-line2-border: #ddd; +$note-icon-gutter-width: 55px; /* @@ -362,7 +380,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5); * Avatar */ $avatar_radius: 50%; -$avatar-border: rgba(0, 0, 0, .1); +$avatar-border: $gray-normal; +$avatar-border-hover: $gray-darker; +$avatar-background: $gray-lightest; $gl-avatar-size: 40px; /* @@ -434,6 +454,7 @@ $logs-p-color: #333; /* * Forms */ +$input-height: 34px; $input-danger-bg: #f2dede; $input-danger-border: $red-400; $input-group-addon-bg: #f7f8fa; @@ -550,7 +571,7 @@ $kdb-color: #555; $kdb-border: #ccc; $kdb-border-bottom: #bbb; $kdb-shadow: #bbb; -$body-text-shadow: rgba(255,255,255,0.01); +$body-text-shadow: rgba(255, 255, 255, 0.01); /* * UI Dev Kit @@ -566,6 +587,12 @@ $stage-hover-border: #d1e7fc; $action-icon-color: #d6d6d6; /* +Pipeline Schedules +*/ +$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); + + +/* Filtered Search */ $filter-name-resting-color: #f8f8f8; @@ -585,3 +612,15 @@ Convdev Index $color-high-score: $green-400; $color-average-score: $orange-400; $color-low-score: $red-400; + +/* +Performance Bar +*/ +$perf-bar-text: #999; +$perf-bar-production: #222; +$perf-bar-staging: #291430; +$perf-bar-development: #4c1210; +$perf-bar-bucket-bg: #111; +$perf-bar-bucket-color: #ccc; +$perf-bar-bucket-box-shadow-from: rgba($white-light, .2); +$perf-bar-bucket-box-shadow-to: rgba($black, .25); diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index 1c1392f8f67..b1ff2659131 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -3,6 +3,7 @@ color: $gl-text-color; border: 1px solid $border-color; border-radius: $border-radius-default; + margin-bottom: $gl-padding; .well-segment { padding: $gl-padding; @@ -21,6 +22,11 @@ font-size: 12px; } } + + &.admin-well h4 { + border-bottom: 1px solid $border-color; + padding-bottom: 8px; + } } .icon-container { @@ -53,6 +59,14 @@ padding: 15px; } +.dark-well { + background-color: $gray-normal; + + .btn { + width: 100%; + } +} + .well-centered { h1 { font-weight: normal; diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 1daa10aef24..578f1902cce 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -113,7 +113,7 @@ $white-gc-bg: #eaf2f5; border-color: $line-removed-dark; a { - color: scale-color($line-number-old,$red: -30%, $green: -30%, $blue: -30%); + color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); } } @@ -122,7 +122,7 @@ $white-gc-bg: #eaf2f5; border-color: $line-added-dark; a { - color: scale-color($line-number-new,$red: -30%, $green: -30%, $blue: -30%); + color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); } } @@ -163,7 +163,7 @@ $white-gc-bg: #eaf2f5; background-color: $line-removed; &::before { - color: scale-color($line-number-old,$red: -30%, $green: -30%, $blue: -30%); + color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); } span.idiff { @@ -175,7 +175,7 @@ $white-gc-bg: #eaf2f5; background-color: $line-added; &::before { - color: scale-color($line-number-new,$red: -30%, $green: -30%, $blue: -30%); + color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); } span.idiff { diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss new file mode 100644 index 00000000000..1c4a84de7ec --- /dev/null +++ b/app/assets/stylesheets/new_nav.scss @@ -0,0 +1,445 @@ +@import "framework/variables"; +@import 'framework/tw_bootstrap_variables'; +@import "bootstrap/variables"; + +header.navbar-gitlab-new { + color: $white-light; + background: linear-gradient(to right, $indigo-900, $indigo-800); + border-bottom: 0; + + .header-content { + padding-left: 0; + + .title-container { + align-items: stretch; + padding-top: 0; + overflow: visible; + } + + .title { + display: flex; + padding-right: 0; + color: currentColor; + + img { + height: 28px; + margin-right: 10px; + } + + > a { + display: flex; + align-items: center; + padding-right: $gl-padding; + padding-left: $gl-padding; + margin-left: -$gl-padding; + + @media (min-width: $screen-sm-min) { + padding-right: $gl-padding; + padding-left: $gl-padding; + } + + svg { + margin-top: -3px; + + @media (min-width: $screen-sm-min) { + margin-right: 10px; + } + } + + .logo-text { + line-height: initial; + + svg { + width: 55px; + height: 15px; + margin: 0; + fill: $white-light; + } + } + + &:hover, + &:focus { + .logo-text svg { + fill: $tanuki-yellow; + } + } + } + } + + .dropdown.open { + > a { + border-bottom-color: $white-light; + } + } + + .dropdown-menu { + margin-top: 4px; + min-width: 130px; + + @media (max-width: $screen-xs-max) { + left: auto; + right: 0; + } + } + } + + .navbar-collapse { + padding-left: 0; + color: $indigo-200; + box-shadow: 0; + + @media (max-width: $screen-xs-max) { + margin-left: -$gl-padding; + margin-right: -10px; + } + + .dropdown-bold-header { + color: initial; + } + + .nav { + > li:not(.hidden-xs) a { + @media (max-width: $screen-xs-max) { + margin-left: 0; + min-width: 100%; + } + } + } + } + + .container-fluid { + .navbar-toggle { + min-width: 45px; + padding: 6px $gl-padding; + margin-right: -7px; + font-size: 14px; + text-align: center; + color: currentColor; + border-left: 1px solid lighten($indigo-700, 10%); + + &:hover, + &:focus, + &.active { + color: currentColor; + background-color: transparent; + } + } + + .navbar-nav { + @media (max-width: $screen-xs-max) { + display: flex; + padding-right: 10px; + } + + li { + .badge { + box-shadow: none; + font-weight: 600; + } + } + } + + .nav > li { + &.header-user { + @media (max-width: $screen-xs-max) { + padding-left: 10px; + } + } + + > a { + background: none; + will-change: color; + + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $indigo-200; + } + } + + &:hover, + &:focus { + color: $white-light; + opacity: 1; + + > svg { + fill: $white-light; + } + + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $white-light; + } + } + } + } + } + } +} + +.navbar-sub-nav { + display: flex; + margin-bottom: 0; + color: $indigo-200; + + > li { + > a:hover, + > a:focus { + box-shadow: inset 0 -3px 0 rgba($indigo-200, .4); + text-decoration: none; + outline: 0; + color: $white-light; + } + + &.active > a { + box-shadow: inset 0 -3px 0 $indigo-500; + color: $white-light; + font-weight: 700; + } + + > a { + display: block; + padding: 16px 10px; + font-size: 13px; + color: currentColor; + box-shadow: inset 0 0 0 transparent; + will-change: box-shadow; + transition: box-shadow 0.15s; + + @media (min-width: $screen-sm-min) { + padding: 15px $gl-padding; + font-size: 14px; + } + } + } + + .dropdown-chevron { + position: relative; + top: -1px; + font-size: 10px; + } +} + +.header-user .dropdown-menu-nav, +.header-new .dropdown-menu-nav { + margin-top: 4px; +} + +.search { + form { + border: 0; + background-color: rgba($indigo-200, .2); + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s, background-color ease-in-out 0.15s; + + &:hover { + background-color: rgba($indigo-200, .3); + box-shadow: none; + } + } + + &.search-active form { + background-color: rgba($indigo-200, .3); + box-shadow: none; + } + + .search-input { + color: $white-light; + background: none; + } + + .search-input::placeholder { + color: rgba($indigo-200, .8); + } + + .location-badge { + font-size: 12px; + color: $indigo-100; + background-color: rgba($indigo-200, .1); + transition: color 0.15s; + will-change: color; + margin: -4px 4px -4px -4px; + line-height: 25px; + padding: 4px 8px; + border-radius: 2px 0 0 2px; + border-right: 1px solid $indigo-800; + height: 34px; + } + + .search-input-wrap { + .search-icon, + .clear-icon { + color: rgba($indigo-200, .8); + } + } + + &.search-active { + .location-badge { + color: $white-light; + background-color: rgba($indigo-200, .2); + } + + .search-input-wrap { + .search-icon { + color: rgba($indigo-200, .8); + } + + .clear-icon { + color: $white-light; + } + } + } +} + +.breadcrumbs { + display: flex; + min-height: 61px; + color: $gl-text-color; + border-bottom: 1px solid $border-color; + + .dropdown-toggle-caret { + position: relative; + top: -1px; + padding: 0 5px; + color: $gl-text-color-secondary; + font-size: 10px; + line-height: 1; + background: none; + border: 0; + + &:focus { + outline: 0; + } + } + + // TODO: fallback to global style + .dropdown-menu { + li { + padding: 0 1px; + + a { + border-radius: 0; + padding: 8px 16px; + + &.is-focused, + &:hover, + &:active, + &:focus { + background-color: $gray-darker; + } + } + } + } +} + +.breadcrumbs-container { + display: flex; + width: 100%; + position: relative; + align-items: center; + + .dropdown-menu-projects { + margin-top: -$gl-padding; + margin-left: $gl-padding; + } +} + +.breadcrumbs-links { + flex: 1; + min-width: 0; + align-self: center; + color: $gl-text-color-quaternary; + + a { + color: $gl-text-color-secondary; + + &:not(:first-child), + &.group-path { + margin-left: 4px; + } + + &:not(:last-of-type), + &.group-path { + margin-right: 3px; + } + } + + .title { + display: inline-block; + + > a { + &:last-of-type:not(:first-child) { + font-weight: 600; + } + } + } + + .avatar-tile { + margin-right: 5px; + border: 1px solid $border-color; + border-radius: 50%; + vertical-align: sub; + + &.identicon { + float: left; + width: 16px; + height: 16px; + margin-top: 2px; + font-size: 10px; + } + } + + .text-expander { + margin-left: 4px; + margin-right: 4px; + + > i { + position: relative; + top: 1px; + } + } +} + +.breadcrumbs-extra { + flex: 0 0 auto; + margin-left: auto; +} + +.breadcrumbs-sub-title { + margin: 2px 0; + font-size: 16px; + font-weight: normal; + line-height: 1; + + ul { + margin: 0; + } + + li { + display: inline-block; + + &:not(:last-child) { + &::after { + content: "/"; + margin: 0 2px 0 5px; + color: rgba($black, .65); + } + } + + &:last-child a { + font-weight: 600; + } + } + + a { + color: $gl-text-color; + } +} + +.top-area { + .nav-controls-new-nav { + .dropdown { + @media (min-width: $screen-sm-min) { + margin-right: 0; + } + } + } +} diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss new file mode 100644 index 00000000000..54f3e8d882c --- /dev/null +++ b/app/assets/stylesheets/new_sidebar.scss @@ -0,0 +1,289 @@ +@import "framework/variables"; +@import 'framework/tw_bootstrap_variables'; +@import "bootstrap/variables"; + +$active-background: rgba(0, 0, 0, .04); +$active-border: $indigo-500; +$active-color: $indigo-700; +$active-hover-background: $active-background; +$active-hover-color: $gl-text-color; +$inactive-badge-background: rgba(0, 0, 0, .08); +$hover-background: $indigo-700; +$hover-color: $white-light; +$inactive-color: $gl-text-color-secondary; +$new-sidebar-width: 220px; + +.page-with-new-sidebar { + @media (min-width: $screen-sm-min) { + padding-left: $new-sidebar-width; + } + + // Override position: absolute + .right-sidebar { + position: fixed; + height: 100%; + } + + .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { + padding: 10px 0 15px; + } +} + +.context-header { + position: relative; + + a { + border-bottom: 1px solid $border-color; + font-weight: 600; + display: flex; + align-items: center; + padding: 10px 16px 10px 10px; + color: $gl-text-color; + + @media (max-width: $screen-xs-max) { + padding-right: 30px; + } + + &:hover { + background-color: $hover-background; + color: $hover-color; + border-color: $hover-background; + + .avatar-container { + border-color: transparent; + } + + .settings-avatar { + background-color: $indigo-500; + + i { + color: $hover-color; + } + } + } + } + + .avatar-container { + flex: 0 0 40px; + background-color: $white-light; + } + + .project-title, + .group-title { + overflow: hidden; + text-overflow: ellipsis; + } + + + &:hover { + .close-nav-button { + color: $white-light; + } + } + + .close-nav-button { + display: none; + position: absolute; + top: 0; + right: 0; + height: 100%; + background-color: transparent; + border: 0; + padding: 0 10px; + + @media (max-width: $screen-xs-max) { + display: block; + } + + &:hover { + color: $gl-text-color; + } + } +} + +.settings-avatar { + background-color: $white-light; + + i { + font-size: 20px; + width: 100%; + color: $gl-text-color-secondary; + text-align: center; + align-self: center; + } +} + +.nav-sidebar { + position: fixed; + z-index: 400; + width: $new-sidebar-width; + transition: left $sidebar-transition-duration; + top: $header-height; + bottom: 0; + left: 0; + overflow: auto; + background-color: $gray-normal; + box-shadow: inset -2px 0 0 $border-color; + + &.nav-sidebar-expanded { + left: 0; + } + + a { + transition: none; + text-decoration: none; + } + + ul { + padding-left: 0; + list-style: none; + } + + li { + white-space: nowrap; + + a { + display: block; + padding: 12px 16px; + color: $inactive-color; + } + } + + li.active { + box-shadow: inset 4px 0 0 $active-border; + + > a { + color: $active-color; + font-weight: 700; + } + } + + @media (max-width: $screen-xs-max) { + left: (-$new-sidebar-width); + } +} + +.with-performance-bar .nav-sidebar { + top: $header-height + $performance-bar-height; +} + +.sidebar-sub-level-items { + display: none; + padding-bottom: 8px; + + > li { + a { + padding: 8px 16px 8px 24px; + + &:hover, + &:focus { + background: $active-hover-background; + color: $active-hover-color; + } + } + + &.active { + a { + &, + &:hover, + &:focus { + background: $active-background; + color: $active-color; + } + } + } + } +} + +.sidebar-top-level-items { + > li { + .badge { + float: right; + background-color: $inactive-badge-background; + color: $inactive-color; + } + + &.active { + background: $active-background; + + .badge { + color: $active-color; + font-weight: 600; + } + + .sidebar-sub-level-items { + display: block; + } + } + + > a:hover { + background-color: $hover-background; + color: $hover-color; + + .badge { + background-color: $indigo-500; + color: $hover-color; + } + } + } +} + +.toggle-mobile-nav { + display: none; + background-color: transparent; + border: 0; + padding: 6px 16px; + margin: 0 16px 0 -15px; + height: 46px; + border-right: 1px solid $gl-text-color-quaternary; + + i { + font-size: 20px; + color: $gl-text-color-secondary; + } + + @media (max-width: $screen-xs-max) { + display: inline-block; + } +} + +.mobile-overlay { + display: none; + + &.mobile-nav-open { + display: block; + position: fixed; + background-color: $black-transparent; + height: 100%; + width: 100%; + z-index: 300; + } +} + + +// Make issue boards full-height now that sub-nav is gone + +.boards-list { + height: calc(100vh - #{$header-height}); + + @media (min-width: $screen-sm-min) { + height: 475px; // Needed for PhantomJS + // scss-lint:disable DuplicateProperty + height: calc(100vh - 180px); + // scss-lint:enable DuplicateProperty + } +} + +.with-performance-bar .boards-list { + height: calc(100vh - #{$header-height} - #{$performance-bar-height}); +} + + +// Change color of all horizontal tabs to match the new indigo color +.nav-links li.active a { + border-bottom-color: $active-border; + + .badge { + font-weight: 600; + } +} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 85109fec91a..6039cda96d8 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -11,7 +11,7 @@ .is-dragging { // Important because plugin sets inline CSS - opacity: 1!important; + opacity: 1 !important; * { -webkit-user-select: none; @@ -19,8 +19,8 @@ -ms-user-select: none; user-select: none; // !important to make sure no style can override this when dragging - cursor: -webkit-grabbing!important; - cursor: grabbing!important; + cursor: -webkit-grabbing !important; + cursor: grabbing !important; } } @@ -431,7 +431,10 @@ margin: 5px; } -.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar { +.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar, +.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar { + position: absolute; + &.right-sidebar { top: 0; bottom: 0; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 7eee0a71c66..acf3719e9d2 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -6,26 +6,26 @@ @keyframes blinking-dots { 0% { background-color: rgba($white-light, 1); - box-shadow: 12px 0 0 0 rgba($white-light,0.2), - 24px 0 0 0 rgba($white-light,0.2); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), + 24px 0 0 0 rgba($white-light, 0.2); } 25% { background-color: rgba($white-light, 0.4); - box-shadow: 12px 0 0 0 rgba($white-light,2), - 24px 0 0 0 rgba($white-light,0.2); + box-shadow: 12px 0 0 0 rgba($white-light, 2), + 24px 0 0 0 rgba($white-light, 0.2); } 75% { background-color: rgba($white-light, 0.4); - box-shadow: 12px 0 0 0 rgba($white-light,0.2), - 24px 0 0 0 rgba($white-light,1); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), + 24px 0 0 0 rgba($white-light, 1); } 100% { background-color: rgba($white-light, 1); - box-shadow: 12px 0 0 0 rgba($white-light,0.2), - 24px 0 0 0 rgba($white-light,0.2); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), + 24px 0 0 0 rgba($white-light, 0.2); } } @@ -37,65 +37,78 @@ } .build-page { - .sticky { - position: absolute; - left: 0; - right: 0; + .build-trace-container { + position: relative; } - .build-trace-container { - position: absolute; - top: 225px; - left: 15px; - bottom: 10px; + .build-trace { background: $black; color: $gray-darkest; - font-family: $monospace_font; + white-space: pre; + overflow-x: auto; font-size: 12px; + border-radius: 0; + border: none; - &.sidebar-expanded { - right: 305px; + .bash { + display: block; } + } - &.sidebar-collapsed { - right: 16px; + .top-bar { + height: 35px; + display: flex; + justify-content: flex-end; + background: $gray-light; + border: 1px solid $border-color; + color: $gl-text-color; + position: sticky; + position: -webkit-sticky; + top: $header-height; + + &.affix { + top: $header-height; } - code { - background: $black; - color: $gray-darkest; + // with sidebar + &.affix.sidebar-expanded { + right: 306px; + left: 16px; } - .top-bar { + // without sidebar + &.affix.sidebar-collapsed { + right: 16px; + left: 16px; + } + + &.affix-top { + position: absolute; + right: 0; + left: 0; top: 0; - height: 35px; - display: flex; - justify-content: flex-end; - background: $gray-light; - border: 1px solid $border-color; - color: $gl-text-color; + } - .truncated-info { - margin: 0 auto; - align-self: center; + .truncated-info { + margin: 0 auto; + align-self: center; - .truncated-info-size { - margin: 0 5px; - } + .truncated-info-size { + margin: 0 5px; + } - .raw-link { - color: $gl-text-color; - margin-left: 5px; - text-decoration: underline; - } + .raw-link { + color: $gl-text-color; + margin-left: 5px; + text-decoration: underline; } } .controllers { display: flex; - align-self: center; font-size: 15px; - margin-bottom: 4px; + justify-content: center; + align-items: center; svg { height: 15px; @@ -103,17 +116,9 @@ fill: $gl-text-color; } - .controllers-buttons, - .btn-scroll { - color: $gl-text-color; - height: 15px; - vertical-align: middle; - padding: 0; - width: 12px; - } - .controllers-buttons { - margin: 1px 10px; + color: $gl-text-color; + margin: 0 10px; } .btn-scroll.animate { @@ -143,16 +148,6 @@ } } - .bash { - top: 35px; - left: 10px; - bottom: 0; - overflow-y: scroll; - overflow-x: hidden; - padding: 10px 20px 20px 5px; - white-space: pre; - } - .environment-information { border: 1px solid $border-color; padding: 8px $gl-padding 12px; @@ -177,6 +172,16 @@ } } +.with-performance-bar .build-page { + .top-bar { + top: $header-height + $performance-bar-height; + + &.affix { + top: $header-height + $performance-bar-height; + } + } +} + .build-header { .ci-header-container, .header-action-buttons { @@ -250,6 +255,10 @@ } } + .block-last { + padding: 16px 0; + } + .trigger-build-variable { color: $code-color; } @@ -399,6 +408,7 @@ .build-light-text { color: $gl-text-color-secondary; + word-wrap: break-word; } .build-gutter-toggle { diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 9db0f2075cb..cd9f2d787c5 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -54,7 +54,11 @@ .mr-widget-pipeline-graph { display: inline-block; vertical-align: middle; - margin: 0 -6px 0 0; + margin-right: 4px; + + .stage-cell .stage-container { + margin: 3px 3px 3px 0; + } .dropdown-menu { margin-top: 11px; @@ -250,8 +254,8 @@ } .committed_ago { - float: right; @extend .cgray; + float: right; } } } @@ -279,3 +283,63 @@ color: $gl-text-color; } } + + +.gpg-status-box { + &.valid { + @include green-status-color; + } + + &.invalid { + @include status-color($gray-dark, $gray, $common-gray-dark); + border-color: $common-gray-light; + } +} + +.gpg-popover-status { + display: flex; + align-items: center; + font-weight: normal; + line-height: 1.5; +} + +.gpg-popover-icon { + // same margin as .s32.avatar + margin-right: $btn-side-margin; + + &.valid { + svg { + border: 1px solid $brand-success; + + fill: $brand-success; + } + } + + &.invalid { + svg { + border: 1px solid $common-gray-light; + + fill: $common-gray-light; + } + } + + svg { + width: 32px; + height: 32px; + border-radius: 50%; + vertical-align: middle; + } +} + +.gpg-popover-user-link { + display: flex; + align-items: center; + margin-bottom: $gl-padding / 2; + text-decoration: none; + color: $gl-text-color; +} + +.commit .gpg-popover-help-link { + display: block; + color: $link-color; +} diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 3039732ca5b..eeb90759f10 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -24,9 +24,9 @@ .col-headers { ul { + @include clearfix; margin: 0; padding: 0; - @include clearfix; } li { @@ -189,8 +189,8 @@ } li { - list-style-type: none; @include clearfix; + list-style-type: none; } .stage-nav-item { @@ -281,11 +281,11 @@ } .stage-event-item { + @include clearfix; list-style-type: none; padding: 0 0 $gl-padding; margin: 0 $gl-padding $gl-padding; border-bottom: 1px solid $gray-darker; - @include clearfix; &:last-child { border-bottom: none; @@ -307,9 +307,9 @@ &.issue-title, &.commit-title, &.merge-merquest-title { + @include text-overflow(); max-width: 100%; display: block; - @include text-overflow(); a { color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b58922626fa..398fd4444ea 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -20,8 +20,6 @@ } .diff-content { - overflow: auto; - overflow-y: hidden; background: $white-light; color: $gl-text-color; border-radius: 0 0 3px 3px; @@ -93,6 +91,7 @@ .old_line, .new_line { + @include user-select(none); margin: 0; border: none; padding: 0 5px; @@ -101,7 +100,6 @@ min-width: 35px; max-width: 50px; width: 35px; - @include user-select(none); a { float: left; @@ -356,12 +354,12 @@ } &.active { + cursor: default; + color: $gl-text-color; + &:hover { text-decoration: none; } - - cursor: default; - color: $gl-text-color; } &.disabled { @@ -476,6 +474,7 @@ height: 19px; width: 19px; margin-left: -15px; + z-index: 100; &:hover { .diff-comment-avatar, @@ -491,7 +490,7 @@ transform: translateX((($i * $x-pos) - $x-pos)); &:hover { - transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2); + transform: translateX((($i * $x-pos) - $x-pos)); } } } @@ -542,6 +541,7 @@ height: 19px; padding: 0; transition: transform .1s ease-out; + z-index: 100; svg { position: absolute; @@ -555,10 +555,6 @@ fill: $white-light; } - &:hover { - transform: scale(1.2); - } - &:focus { outline: 0; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 89bd437b362..00ebf4e26ac 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -83,6 +83,7 @@ .avatar { float: none; + margin-right: 0; } } @@ -139,23 +140,6 @@ } } -.prometheus-graph { - text { - fill: $gl-text-color; - stroke-width: 0; - } - - .label-axis-text, - .text-metric-usage { - fill: $black; - font-weight: 500; - } - - .legend-axis-text { - fill: $black; - } -} - .x-axis path, .y-axis path, .label-x-axis-line, @@ -203,7 +187,7 @@ } .text-metric { - font-weight: 600; + font-size: 12px; } .selected-metric-line { @@ -213,20 +197,15 @@ .deployment-line { stroke: $black; - stroke-width: 2; + stroke-width: 1; } .deploy-info-text { dominant-baseline: text-before-edge; } -.text-metric-bold { - font-weight: 600; -} - .prometheus-state { margin-top: 10px; - display: none; .state-button-section { margin-top: 10px; @@ -241,3 +220,78 @@ width: 38px; } } + +.prometheus-panel { + margin-top: 20px; +} + +.prometheus-svg-container { + position: relative; + height: 0; + width: 100%; + padding: 0; + padding-bottom: 100%; +} + +.prometheus-svg-container > svg { + position: absolute; + height: 100%; + width: 100%; + left: 0; + top: 0; + + text { + fill: $gl-text-color; + stroke-width: 0; + } + + .text-metric-bold { + font-weight: 600; + } + + .label-axis-text, + .text-metric-usage { + fill: $black; + font-weight: 500; + font-size: 12px; + } + + .legend-axis-text { + fill: $black; + } + + .tick > text { + font-size: 12px; + } + + .text-metric-title { + font-size: 12px; + } + + .y-label-text, + .x-label-text { + fill: $gray-darkest; + } + + .axis-tick { + stroke: $gray-darker; + } + + @media (max-width: $screen-sm-max) { + .label-axis-text, + .text-metric-usage, + .legend-axis-text { + font-size: 8px; + } + + .tick > text { + font-size: 8px; + } + } +} + +.prometheus-row { + h5 { + font-size: 16px; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 20f2eec9af5..6da14320914 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -11,7 +11,9 @@ .commit-box, .info-well, .commit-ci-menu, - .files-changed { + .files-changed, + .limited-header-width, + .limited-width-notes { @extend .fixed-width-container; } @@ -198,7 +200,6 @@ right: 0; transition: width .3s; background: $gray-light; - padding: 0 20px; z-index: 200; overflow: hidden; @@ -210,6 +211,10 @@ -webkit-overflow-scrolling: touch; } + &.affix-top .issuable-sidebar { + height: 100%; + } + &.right-sidebar-expanded { width: $gutter_width; @@ -222,10 +227,20 @@ } } + .issuable-sidebar { + padding: 0 20px; + } + .issuable-sidebar-header { padding-top: 10px; } + &:not(.issue-boards-sidebar):not([data-signed-in]) { + .issuable-sidebar-header { + display: none; + } + } + .assign-yourself .btn-link { padding-left: 0; } @@ -247,6 +262,10 @@ border-left: 1px solid $border-gray-normal; } + .title .gutter-toggle { + margin-top: 0; + } + .assignee .avatar { float: left; margin-right: 10px; @@ -331,13 +350,9 @@ display: none; } - .avatar:hover, - .avatar-counter:hover { - border-color: $issuable-sidebar-color; - } - .avatar-counter:hover { color: $issuable-sidebar-color; + border-color: $issuable-sidebar-color; } .btn-clipboard { @@ -430,6 +445,14 @@ } } +.with-performance-bar .right-sidebar { + top: $header-height + $performance-bar-height; + + .issuable-sidebar { + height: calc(100% - #{$header-height} - #{$performance-bar-height}); + } +} + .detail-page-description { padding: 16px 0; @@ -585,7 +608,38 @@ .issue-info-container { -webkit-flex: 1; flex: 1; + display: flex; padding-right: $gl-padding; + + .issue-main-info { + flex: 1 auto; + margin-right: 10px; + } + + .issuable-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + flex: 1 0 auto; + + .controls { + margin-bottom: 2px; + line-height: 20px; + padding: 0; + } + + .issue-updated-at { + line-height: 20px; + } + } + + @media(max-width: $screen-xs-max) { + .issuable-meta { + .controls li { + margin-right: 0; + } + } + } } .issue-check { @@ -597,6 +651,30 @@ vertical-align: text-top; } } + + .issuable-milestone, + .issuable-info, + .task-status, + .issuable-updated-at { + font-weight: normal; + color: $gl-text-color-secondary; + + a { + color: $gl-text-color; + + .fa { + color: $gl-text-color-secondary; + } + } + } + + @media(max-width: $screen-md-max) { + .task-status, + .issuable-due-date, + .project-ref-path { + display: none; + } + } } } @@ -729,3 +807,26 @@ } } } + +.issuable-close-button, +.issuable-close-toggle { + @include transition(border-color, color); +} + +.issuable-close-dropdown { + .dropdown-menu { + min-width: 270px; + left: auto; + right: 0; + } + + .description { + .text { + margin: 0; + } + } + + .dropdown-toggle > .icon { + margin: 0 3px; + } +} diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index c10588ac58e..ee48f7a3626 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -138,6 +138,7 @@ .fa { font-size: 18px; vertical-align: middle; + pointer-events: none; } &:hover { @@ -278,5 +279,9 @@ .label-link { display: inline-block; - vertical-align: text-top; + vertical-align: top; + + .label { + vertical-align: inherit; + } } diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 4be0e133b69..e7c07ef67f0 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -54,8 +54,6 @@ @media (min-width: $screen-sm-min) { display: -webkit-flex; display: flex; - width: 400px; - max-width: 50%; } } @@ -65,7 +63,6 @@ @media (min-width: $screen-sm-min) { display: -webkit-flex; display: flex; - width: 100%; margin-top: 3px; } } @@ -81,18 +78,10 @@ .member-form-control { @media (max-width: $screen-xs-max) { - padding: 5px 0; + padding-bottom: 5px; margin-left: 0; margin-right: 0; } - - @media (min-width: $screen-sm-min) { - width: 50%; - } - - .dropdown-menu-toggle { - width: 100%; - } } .member-access-text { @@ -136,10 +125,6 @@ width: 250px; } - @media (min-width: $screen-md-min) { - width: 350px; - } - &.input-short { @media (min-width: $screen-md-min) { width: 170px; @@ -220,3 +205,102 @@ } } } + +.content-list.members-list li { + display: flex; + justify-content: space-between; + + .list-item-name { + float: none; + display: flex; + flex: 1; + } + + .user-info { + padding-right: 10px; + } + + .member { + font-weight: bold; + overflow-wrap: break-word; + word-break: break-all; + } + + .member-group-link { + display: inline-block; + } + + .form-control { + width: inherit; + } + + .btn { + align-self: flex-start; + } + + .form-horizontal ~ .btn { + margin-right: 0; + } + + @media (max-width: $screen-xs-max) { + display: block; + + .controls > .btn { + margin-left: 0; + margin-right: 0; + display: block; + } + + .form-control { + width: 100%; + } + + .member-access-text { + line-height: 0; + margin-left: 50px; + } + + .member-controls { + margin-top: 5px; + } + + .form-horizontal { + margin-top: 10px; + } + } +} + +.panel-mobile { + .content-list.members-list li { + display: block; + + .member-controls { + float: none; + display: block; + } + + .dropdown-menu-toggle, + .dropdown-menu, + .form-control, + .list-item-name { + width: 100%; + } + + .dropdown-menu { + margin-top: 0; + } + + .form-horizontal { + display: block; + } + + .member-form-control { + margin: 5px 0; + } + + .btn { + width: 100%; + margin-left: 0; + } + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 2dc7f73a295..4693b2434c7 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -96,7 +96,7 @@ overflow: visible; } - & > span { + > span { padding-right: 4px; } @@ -125,7 +125,7 @@ .dropdown-menu { margin-top: 11px; - z-index: 200; + z-index: 300; } .ci-action-icon-wrapper { @@ -419,7 +419,7 @@ .commit { margin: 0; - padding: 10px 0; + padding: 10px; list-style: none; &:hover { @@ -731,11 +731,11 @@ .merge-request-tabs-holder { top: $header-height; - z-index: 100; + z-index: 200; background-color: $white-light; border-bottom: 1px solid $border-color; - @media(min-width: $screen-sm-min) { + @media (min-width: $screen-sm-min) { position: sticky; position: -webkit-sticky; } @@ -759,6 +759,10 @@ } } +.with-performance-bar .merge-request-tabs-holder { + top: $header-height + $performance-bar-height; +} + .merge-request-tabs { display: flex; margin-bottom: 0; @@ -770,6 +774,12 @@ max-width: $limited-layout-width; margin-left: auto; margin-right: auto; + + .inner-page-scroll-tabs { + background-color: $white-light; + margin-left: -$gl-padding; + padding-left: $gl-padding; + } } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 69fed4e6bf7..cdb1e65e4be 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -148,8 +148,20 @@ padding: 6px 0; } -.notes-form > li { - border: 0; +.notes.notes-form > li.timeline-entry { + @include notes-media('max', $screen-sm-max) { + padding: 0; + } + + .timeline-content { + @include notes-media('max', $screen-sm-max) { + margin: 0; + } + } + + .timeline-entry-inner { + border: 0; + } } .note-edit-form { @@ -344,7 +356,6 @@ color: $white-light; padding-right: 2px; margin-top: 2px; - pointer-events: none; } } @@ -354,56 +365,6 @@ width: 298px; } - .description { - display: inline-block; - white-space: normal; - margin-left: 8px; - padding-right: 33px; - } - - li { - padding-top: 6px; - - & > a { - margin: 0; - padding: 0; - color: inherit; - border-radius: 0; - text-overflow: inherit; - - &:hover, - &:focus { - background-color: inherit; - color: inherit; - } - } - - &:hover, - &:focus { - background-color: $dropdown-hover-color; - color: $white-light; - } - - &.droplab-item-selected i { - visibility: visible; - } - - i { - visibility: hidden; - } - } - - i { - display: inline-block; - vertical-align: top; - padding-top: 2px; - } - - .divider { - margin: 0 8px; - padding: 0; - border-top: $gray-darkest; - } @media (max-width: $screen-xs-max) { display: flex; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 8b7df4dd72b..2bb867052f6 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -14,16 +14,6 @@ ul.notes { margin: 0; padding: 0; - .timeline-content { - margin-left: 55px; - - &.timeline-content-form { - @include notes-media('max', $screen-sm-max) { - margin-left: 0; - } - } - } - .note-created-ago, .note-updated-at { white-space: nowrap; @@ -46,15 +36,47 @@ ul.notes { } } - > li { - padding: $gl-padding $gl-btn-padding; + > li { // .timeline-entry + padding: 0; display: block; position: relative; - border-bottom: 1px solid $white-normal; + border-bottom: 0; + + @include notes-media('min', $screen-sm-min) { + padding-left: $note-icon-gutter-width; + } + + .timeline-entry-inner { + padding: $gl-padding $gl-btn-padding; + border-bottom: 1px solid $white-normal; + } - &:last-child { - // Override `.timeline > li:last-child { border-bottom: none; }` + &:target, + &.target { border-bottom: 1px solid $white-normal; + + &:not(:first-child) { + border-top: 1px solid $white-normal; + margin-top: -1px; + } + + .timeline-entry-inner { + border-bottom: 0; + } + } + + .timeline-icon { + @include notes-media('min', $screen-sm-min) { + margin-left: -$note-icon-gutter-width; + } + } + + .timeline-content { + margin-left: $note-icon-gutter-width; + + @include notes-media('min', $screen-sm-min) { + margin-left: 0; + } } &.being-posted { @@ -73,7 +95,7 @@ ul.notes { } &.note-discussion { - &.timeline-entry { + .timeline-entry-inner { padding: $gl-padding 10px; } } @@ -99,10 +121,11 @@ ul.notes { overflow-y: hidden; .note-text { - word-wrap: break-word; @include md-typography; // Reset ul style types since we're nested inside a ul already @include bulleted-list; + word-wrap: break-word; + ul.task-list { ul:not(.task-list) { padding-left: 1.3em; @@ -152,13 +175,8 @@ ul.notes { .system-note { font-size: 14px; - padding-left: 0; clear: both; - @include notes-media('min', $screen-sm-min) { - margin-left: 65px; - } - .note-header-info { padding-bottom: 0; } @@ -192,13 +210,16 @@ ul.notes { .timeline-icon { float: left; + @include notes-media('min', $screen-sm-min) { + margin-left: 0; + width: auto; + } + svg { width: 16px; height: 16px; fill: $gray-darkest; - position: absolute; - left: 0; - top: 2px; + margin-top: 2px; } } @@ -230,7 +251,7 @@ ul.notes { } .note-text { - & p:first-child { + p:first-child { display: none; } @@ -250,7 +271,7 @@ ul.notes { &::after { content: ''; width: 100%; - height: 67px; + height: 70px; position: absolute; left: 0; bottom: 0; @@ -453,7 +474,7 @@ ul.notes { } .more-actions { - display: inline; + display: inline-block; .tooltip { white-space: nowrap; @@ -608,8 +629,14 @@ ul.notes { * Line note button on the side of diffs */ +.line_holder .is-over:not(.no-comment-btn) { + .add-diff-note { + opacity: 1; + } +} + .add-diff-note { - display: none; + opacity: 0; margin-top: -2px; border-radius: 50%; background: $white-light; @@ -622,13 +649,11 @@ ul.notes { width: 23px; height: 23px; border: 1px solid $blue-500; - transition: transform .1s ease-in-out; &:hover { background: $blue-500; border-color: $blue-600; color: $white-light; - transform: scale(1.15); } &:active { @@ -639,15 +664,12 @@ ul.notes { .discussion-body, .diff-file { .notes .note { - padding-left: $gl-padding; - padding-right: $gl-padding; - - &.system-note { - padding-left: 0; + border-bottom: 1px solid $white-normal; - @media (min-width: $screen-sm-min) { - margin-left: 70px; - } + .timeline-entry-inner { + padding-left: $gl-padding; + padding-right: $gl-padding; + border-bottom: none; } } } diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index 595eb40fec7..dc1654e006e 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -1,7 +1,7 @@ .js-pipeline-schedule-form { .dropdown-select, .dropdown-menu-toggle { - width: 100%!important; + width: 100% !important; } .gl-field-error { @@ -74,3 +74,84 @@ margin-right: 3px; } } + +.pipeline-variable-list { + margin-left: 0; + margin-bottom: 0; + padding-left: 0; + list-style: none; + clear: both; +} + +.pipeline-variable-row { + display: flex; + align-items: flex-end; + + &:not(:last-child) { + margin-bottom: $gl-btn-padding; + } + + @media (max-width: $screen-sm-max) { + padding-right: $gl-col-padding; + } + + &:last-child { + .pipeline-variable-row-remove-button { + display: none; + } + + @media (max-width: $screen-sm-max) { + .pipeline-variable-value-input { + margin-right: $pipeline-variable-remove-button-width; + } + } + + @media (max-width: $screen-xs-max) { + .pipeline-variable-row-body { + margin-right: $pipeline-variable-remove-button-width; + } + } + } +} + +.pipeline-variable-row-body { + display: flex; + width: calc(75% - #{$gl-col-padding}); + padding-left: $gl-col-padding; + + @media (max-width: $screen-sm-max) { + width: 100%; + } + + @media (max-width: $screen-xs-max) { + display: block; + } +} + +.pipeline-variable-key-input { + margin-right: $gl-btn-padding; + + @media (max-width: $screen-xs-max) { + margin-bottom: $gl-btn-padding; + } +} + +.pipeline-variable-row-remove-button { + @include transition(color); + flex-shrink: 0; + display: flex; + justify-content: center; + align-items: center; + width: $pipeline-variable-remove-button-width; + height: $input-height; + padding: 0; + background: transparent; + border: 0; + color: $gl-text-color-secondary; + + &:hover, + &:focus { + outline: none; + color: $gl-text-color; + } +} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a85ba3a5955..d3862df20d3 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -133,7 +133,7 @@ overflow: hidden; display: inline-block; white-space: nowrap; - vertical-align: top; + vertical-align: middle; text-overflow: ellipsis; } @@ -597,7 +597,7 @@ } // Dropdown button in mini pipeline graph -.mini-pipeline-graph-dropdown-toggle { +button.mini-pipeline-graph-dropdown-toggle { border-radius: 100px; background-color: $white-light; border-width: 1px; @@ -608,6 +608,7 @@ padding: 0; transition: all 0.2s linear; position: relative; + vertical-align: middle; > .fa.fa-caret-down { position: absolute; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index c207159f606..14ad06b0ac2 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -286,8 +286,7 @@ table.u2f-registrations { } .user-callout { - margin: 0 auto; - max-width: $screen-lg-min; + margin: 20px -5px 0; .bordered-box { border: 1px solid $blue-300; @@ -377,3 +376,41 @@ table.u2f-registrations { } } } + +.nav-wip { + border: 1px solid $blue-500; + background: $blue-25; + padding: $gl-padding; + margin-bottom: $gl-padding; + + a { + color: $blue-500; + } + + p:last-child { + margin-bottom: 0; + } +} + +.gpg-email-badge { + display: inline; + margin-right: $gl-padding / 2; + + .gpg-email-badge-email { + display: inline; + margin-right: $gl-padding / 4; + } + + .label-verification-status { + border-width: 1px; + border-style: solid; + + &.verified { + @include green-status-color; + } + + &.unverified { + @include status-color($gray-dark, $gray, $common-gray-dark); + } + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 062665bc634..b3a90dff89a 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -26,7 +26,7 @@ margin-bottom: 5px; } - & > .form-group { + > .form-group { padding-left: 0; } @@ -83,7 +83,7 @@ border: 1px solid $border-color; } - & + .select2 a { + + .select2 a { border-top-left-radius: 0; border-bottom-left-radius: 0; } @@ -377,10 +377,11 @@ a.deploy-project-label { } .breadcrumb.repo-breadcrumb { + flex: 1; padding: 0; background: transparent; border: none; - line-height: 36px; + line-height: 34px; margin: 0; > li + li::before { @@ -482,11 +483,12 @@ a.deploy-project-label { .project-stats { font-size: 0; text-align: center; + max-width: 100%; + border-bottom: 1px solid $border-color; .nav { padding-top: 12px; padding-bottom: 12px; - border-bottom: 1px solid $border-color; } .nav > li { @@ -585,9 +587,9 @@ pre.light-well { } .project-row { + @include basic-list-stats; display: flex; align-items: center; - @include basic-list-stats; } h3 { @@ -705,6 +707,7 @@ pre.light-well { background-color: transparent; border: 0; text-align: left; + text-overflow: ellipsis; } .protected-branches-list, @@ -740,7 +743,8 @@ pre.light-well { } } -.protected-tags-list { +.protected-tags-list, +.protected-branches-list { .dropdown-menu-toggle { width: 100%; max-width: 300px; diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 9b6ff237557..57c73295d1e 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -33,3 +33,20 @@ font-weight: normal; } } + +.admin-runner-btn-group-cell { + min-width: 150px; + + .btn-sm { + padding: 4px 9px; + } + + .btn-default { + color: $gl-text-color-secondary; + } + + .fa-pause, + .fa-play { + font-size: 11px; + } +} diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 7697a1b1c58..d69a8e0995c 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -126,3 +126,66 @@ margin-left: 5px; } } + +.prometheus-metrics-monitoring { + .panel { + .panel-toggle { + width: 14px; + } + + .badge { + font-size: inherit; + } + + .panel-heading .badge-count { + color: $white-light; + background: $common-gray-dark; + } + + .panel-body { + padding: 0; + } + + .flash-container { + margin-bottom: 0; + cursor: default; + + .flash-notice { + border-radius: 0; + } + } + } + + .loading-metrics, + .empty-metrics { + padding: 30px 10px; + + p, + .btn { + margin-top: 10px; + margin-bottom: 0; + } + } + + .loading-metrics .metrics-load-spinner { + color: $loading-color; + } + + .metrics-list { + margin-bottom: 0; + + li { + padding: $gl-padding; + + .badge { + margin-left: 5px; + background: $badge-bg; + } + } + + /* Ensure we don't add border if there's only single li */ + li + li { + border-top: 1px solid $border-color; + } + } +} diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 67ad1ae60af..36f622db136 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -1,22 +1,3 @@ -@mixin status-color($color-light, $color-main, $color-dark) { - color: $color-main; - border-color: $color-main; - - &:not(span):hover { - background-color: $color-light; - color: $color-dark; - border-color: $color-dark; - - svg { - fill: $color-dark; - } - } - - svg { - fill: $color-main; - } -} - .ci-status { padding: 2px 7px 4px; border: 1px solid $gray-darker; @@ -41,7 +22,7 @@ } &.ci-success { - @include status-color($green-50, $green-500, $green-700); + @include green-status-color; } &.ci-canceled, diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index de652a79369..d7a9dda3770 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -81,7 +81,7 @@ .todo-title { display: flex; - & > .title-item { + > .title-item { -webkit-flex: 0 0 auto; flex: 0 0 auto; margin: 0 2px; diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index ab63225147f..e0f46172769 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,10 +1,83 @@ .tree-holder { - > .nav-block { - margin: 11px 0; + + .nav-block { + margin: 10px 0; + + @media (min-width: $screen-sm-min) { + display: flex; + + .tree-ref-container { + flex: 1; + } + + .tree-controls { + text-align: right; + + .btn-group { + margin-left: 10px; + } + + .control { + float: left; + margin-left: 10px; + } + } + + .tree-ref-holder { + float: left; + margin-right: 15px; + } + + .repo-breadcrumb { + li:last-of-type { + position: relative; + } + } + + .add-to-tree-dropdown { + position: absolute; + left: 18px; + } + } + } + + @media (max-width: $screen-xs-max) { + .repo-breadcrumb { + margin-top: 10px; + position: relative; + + .dropdown-menu { + min-width: 100%; + width: 100%; + left: inherit; + right: 0; + } + } + + .add-to-tree-dropdown { + position: absolute; + left: 0; + right: 0; + } + + .tree-controls { + margin-bottom: 10px; + + .btn, + .dropdown, + .btn-group { + width: 100%; + } + + .btn { + margin: 10px 0 0; + } + } } .file-finder { - width: 50%; + max-width: 500px; + width: 100%; .file-finder-input { width: 95%; @@ -129,11 +202,28 @@ } } } -} -.tree-ref-holder { - float: left; - margin-right: 15px; + // TODO: fallback to global style + .dropdown-menu:not(.dropdown-menu-selectable) { + li { + padding: 0 1px; + + &.dropdown-header { + padding: 8px 16px; + } + + a { + border-radius: 0; + padding: 8px 16px; + + &:hover, + &:active, + &:focus { + background-color: $gray-darker; + } + } + } + } } .blob-commit-info { @@ -159,16 +249,6 @@ color: $md-link-color; } -.tree-controls { - float: right; - position: relative; - z-index: 2; - - .project-action-button { - margin-left: $btn-side-margin; - } -} - .repo-charts { .sub-header { margin: 20px 0; diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss index 8c87bc3cafd..798e060a261 100644 --- a/app/assets/stylesheets/pages/ui_dev_kit.scss +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -5,13 +5,13 @@ } .example { + padding: 15px; + border: 1px dashed $ui-dev-kit-example-border; + margin-bottom: 15px; + &::before { content: "Example"; color: $ui-dev-kit-example-color; } - - padding: 15px; - border: 1px dashed $ui-dev-kit-example-border; - margin-bottom: 15px; } } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 94d0a39f397..45c21c5d274 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -147,13 +147,13 @@ } ul.wiki-pages-list.content-list { - & ul { + ul { list-style: none; margin-left: 0; padding-left: 15px; } - & ul li { + ul li { padding: 5px 0; } } diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss new file mode 100644 index 00000000000..6e539e39ca1 --- /dev/null +++ b/app/assets/stylesheets/performance_bar.scss @@ -0,0 +1,111 @@ +@import "framework/variables"; +@import "peek/views/performance_bar"; +@import "peek/views/rblineprof"; + +#peek { + position: fixed; + left: 0; + top: 0; + width: 100%; + z-index: 2000; + overflow-x: hidden; + + height: $performance-bar-height; + background: $black; + line-height: $performance-bar-height; + color: $perf-bar-text; + + &.disabled { + display: none; + } + + &.production { + background-color: $perf-bar-production; + } + + &.staging { + background-color: $perf-bar-staging; + } + + &.development { + background-color: $perf-bar-development; + } + + .wrapper { + width: 80%; + height: $performance-bar-height; + margin: 0 auto; + } + + // UI Elements + .bucket { + background: $perf-bar-bucket-bg; + display: inline-block; + padding: 4px 6px; + font-family: Consolas, "Liberation Mono", Courier, monospace; + line-height: 1; + color: $perf-bar-bucket-color; + border-radius: 3px; + box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, inset 0 1px 2px $perf-bar-bucket-box-shadow-to; + + .hidden { + display: none; + } + + &:hover .hidden { + display: inline; + } + } + + strong { + color: $white-light; + } + + table { + color: $black; + + strong { + color: $black; + } + } + + .view { + margin-right: 15px; + float: left; + + &:last-child { + margin-right: 0; + } + } + + .css-truncate { + &.css-truncate-target, + .css-truncate-target { + display: inline-block; + max-width: 125px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: top; + } + + &.expandable:hover .css-truncate-target, + &.expandable:hover.css-truncate-target { + max-width: 10000px !important; + } + } +} + +#modal-peek-pg-queries-content { + color: $black; +} + +.peek-rblineprof-file { + pre.duration { + width: 280px; + } + + .data { + overflow: visible; + } +} diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 136d0c79467..113e6e86bb5 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -37,7 +37,7 @@ ul.notes-form, .issuable-details .content-block-small, .edit-link, .note-action-button { - display: none!important; + display: none !important; } pre { |