diff options
Diffstat (limited to 'app')
431 files changed, 5707 insertions, 2863 deletions
diff --git a/app/assets/images/icon-merge-request-unmerged.svg b/app/assets/images/icon-merge-request-unmerged.svg index c4d8e65122d..d53a7470243 100644 --- a/app/assets/images/icon-merge-request-unmerged.svg +++ b/app/assets/images/icon-merge-request-unmerged.svg @@ -1 +1 @@ -<svg width="12" height="15" viewBox="0 0 12 15" xmlns="http://www.w3.org/2000/svg"><path d="M10.267 11.028V5.167c-.028-.728-.318-1.372-.878-1.923-.56-.55-1.194-.85-1.922-.877h-.934V.5l-2.8 2.8 2.8 2.8V4.233h.934a.976.976 0 0 1 .644.29.88.88 0 0 1 .289.644v5.861a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472zM3.733 3.3a1.86 1.86 0 0 0-1.866-1.867 1.86 1.86 0 0 0-.934 3.472v6.123a1.86 1.86 0 0 0 .933 3.472 1.86 1.86 0 0 0 .934-3.472V4.905c.55-.317.933-.914.933-1.605z" fill-rule="nonzero"/></svg> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m5 5.563v4.875c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-4.875c-1.024-.4-1.75-1.397-1.75-2.563 0-1.519 1.231-2.75 2.75-2.75 1.519 0 2.75 1.231 2.75 2.75 0 1.166-.726 2.162-1.75 2.563m-1 8.687c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25m0-10c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/><path d="m10.501 2c1.381.001 2.499 1.125 2.499 2.506v5.931c1.024.4 1.75 1.397 1.75 2.563 0 1.519-1.231 2.75-2.75 2.75-1.519 0-2.75-1.231-2.75-2.75 0-1.166.726-2.162 1.75-2.563v-5.931c0-.279-.225-.506-.499-.506v.926c0 .346-.244.474-.569.271l-2.952-1.844c-.314-.196-.325-.507 0-.71l2.952-1.844c.314-.196.569-.081.569.271v.93m1.499 12.25c.69 0 1.25-.56 1.25-1.25 0-.69-.56-1.25-1.25-1.25-.69 0-1.25.56-1.25 1.25 0 .69.56 1.25 1.25 1.25"/></svg>
\ No newline at end of file diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index aebda7780e1..d816df831eb 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign, class-methods-use-this */ /* global Pager */ -/* global Cookies */ + +import Cookies from 'js-cookie'; class Activities { constructor() { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 9349918f7a0..4f63c7988f5 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,4 +1,4 @@ -/* global Cookies */ +import Cookies from 'js-cookie'; import emojiMap from 'emojis/digests.json'; import emojiAliases from 'emojis/aliases.json'; @@ -476,10 +476,10 @@ AwardsHandler.prototype.setupSearch = function setupSearch() { this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => { const term = $(e.target).val().trim(); // Clean previous search results - $('ul.emoji-menu-search, h5.emoji-search').remove(); + $('ul.emoji-menu-search, h5.emoji-search-title').remove(); if (term.length > 0) { // Generate a search result block - const h5 = $('<h5 class="emoji-search" />').text('Search results'); + const h5 = $('<h5 class="emoji-search-title"/>').text('Search results'); const foundEmojis = this.searchEmojis(term).show(); const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); $('.emoji-menu-content ul, .emoji-menu-content h5').hide(); diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js index 5e3c45f7e92..20ab2d7e827 100644 --- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js +++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js @@ -1,5 +1,3 @@ -import spreadString from './spread_string'; - // On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/ const flagACodePoint = 127462; // parseInt('1F1E6', 16) const flagZCodePoint = 127487; // parseInt('1F1FF', 16) @@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) { const tone1 = 127995;// parseInt('1F3FB', 16) const tone5 = 127999;// parseInt('1F3FF', 16) function isSkinToneComboEmoji(emojiUnicode) { - return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => { + return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => { const cp = char.codePointAt(0); return cp >= tone1 && cp <= tone5; }); @@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) { // doesn't support the skin tone versions of horse racing const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16) function isHorceRacingSkinToneComboEmoji(emojiUnicode) { - return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && + return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint && isSkinToneComboEmoji(emojiUnicode); } @@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16) function isPersonZwjEmoji(emojiUnicode) { let hasPersonEmoji = false; let hasZwj = false; - spreadString(emojiUnicode).forEach((character) => { + Array.from(emojiUnicode).forEach((character) => { const cp = character.codePointAt(0); if (cp === zwj) { hasZwj = true; diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js deleted file mode 100644 index 327764ec6e9..00000000000 --- a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js +++ /dev/null @@ -1,50 +0,0 @@ -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known -function knownCharCodeAt(givenString, index) { - const str = `${givenString}`; - const end = str.length; - - const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; - let idx = index; - while ((surrogatePairs.exec(str)) != null) { - const li = surrogatePairs.lastIndex; - if (li - 2 < idx) { - idx += 1; - } else { - break; - } - } - - if (idx >= end || idx < 0) { - return NaN; - } - - const code = str.charCodeAt(idx); - - let high; - let low; - if (code >= 0xD800 && code <= 0xDBFF) { - high = code; - low = str.charCodeAt(idx + 1); - // Go one further, since one of the "characters" is part of a surrogate pair - return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; - } - return code; -} - -// See http://stackoverflow.com/a/38901550/796832 -// ES5/PhantomJS compatible version of spreading a string -// -// [...'foo'] -> ['f', 'o', 'o'] -// [...'๐๐ฟ'] -> ['๐', '๐ฟ'] -function spreadString(str) { - const arr = []; - let i = 0; - while (!isNaN(knownCharCodeAt(str, i))) { - const codePoint = knownCharCodeAt(str, i); - arr.push(String.fromCodePoint(codePoint)); - i += 1; - } - return arr; -} - -export default spreadString; diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 92f3bb3ff52..576b8a0425f 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -18,13 +18,13 @@ // Button does not change visibility. If button has icon - it changes chevron style. // // %div.js-toggle-container - // %a.js-toggle-button + // %button.js-toggle-button // %div.js-toggle-content // $('body').on('click', '.js-toggle-button', function(e) { toggleContainer($(this).closest('.js-toggle-container')); - const targetTag = e.target.tagName.toLowerCase(); + const targetTag = e.currentTarget.tagName.toLowerCase(); if (targetTag === 'a' || targetTag === 'button') { e.preventDefault(); } diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js deleted file mode 100644 index ec1c018424d..00000000000 --- a/app/assets/javascripts/blob/blob_ci_yaml.js +++ /dev/null @@ -1,42 +0,0 @@ -/* eslint-disable no-param-reassign, comma-dangle */ -/* global Api */ - -require('./template_selector'); - -((global) => { - class BlobCiYamlSelector extends gl.TemplateSelector { - requestFile(query) { - return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); - } - - requestFileSuccess(file) { - return super.requestFileSuccess(file); - } - } - - global.BlobCiYamlSelector = BlobCiYamlSelector; - - class BlobCiYamlSelectors { - constructor({ editor, $dropdowns } = {}) { - this.editor = editor; - this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector'); - this.initSelectors(); - } - - initSelectors() { - const editor = this.editor; - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new BlobCiYamlSelector({ - editor, - pattern: /(.gitlab-ci.yml)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'), - dropdown: $dropdown - }); - }); - } - } - - global.BlobCiYamlSelectors = BlobCiYamlSelectors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js b/app/assets/javascripts/blob/blob_dockerfile_selector.js deleted file mode 100644 index d4f60cc6ecd..00000000000 --- a/app/assets/javascripts/blob/blob_dockerfile_selector.js +++ /dev/null @@ -1,19 +0,0 @@ -/* global Api */ - -require('./template_selector'); - -(() => { - const global = window.gl || (window.gl = {}); - - class BlobDockerfileSelector extends gl.TemplateSelector { - requestFile(query) { - return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this)); - } - - requestFileSuccess(file) { - return super.requestFileSuccess(file); - } - } - - global.BlobDockerfileSelector = BlobDockerfileSelector; -})(); diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/blob_dockerfile_selectors.js deleted file mode 100644 index 9cee79fa5d5..00000000000 --- a/app/assets/javascripts/blob/blob_dockerfile_selectors.js +++ /dev/null @@ -1,27 +0,0 @@ -(() => { - const global = window.gl || (window.gl = {}); - - class BlobDockerfileSelectors { - constructor({ editor, $dropdowns } = {}) { - this.editor = editor; - this.$dropdowns = $dropdowns || $('.js-dockerfile-selector'); - this.initSelectors(); - } - - initSelectors() { - const editor = this.editor; - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new gl.BlobDockerfileSelector({ - editor, - pattern: /(Dockerfile)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'), - dropdown: $dropdown, - }); - }); - } - } - - global.BlobDockerfileSelectors = BlobDockerfileSelectors; -})(); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 8f6bf162d6e..c9fe23aec75 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,66 +1,63 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, max-len */ +/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */ /* global Dropzone */ -(function() { - this.BlobFileDropzone = (function() { - function BlobFileDropzone(form, method) { - var dropzone, form_dropzone, submitButton; - form_dropzone = form.find('.dropzone'); - Dropzone.autoDiscover = false; - dropzone = form_dropzone.dropzone({ - autoDiscover: false, - autoProcessQueue: false, - url: form.attr('action'), - // Rails uses a hidden input field for PUT - // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails - method: method, - clickable: true, - uploadMultiple: false, - paramName: "file", - maxFilesize: gon.max_file_size || 10, - parallelUploads: 1, - maxFiles: 1, - addRemoveLinks: true, - previewsContainer: '.dropzone-previews', - headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") - }, - init: function() { - this.on('addedfile', function(file) { - $('.dropzone-alerts').html('').hide(); - }); - this.on('success', function(header, response) { - window.location.href = response.filePath; - }); - this.on('maxfilesexceeded', function(file) { - this.removeFile(file); - }); - return this.on('sending', function(file, xhr, formData) { - formData.append('target_branch', form.find('input[name="target_branch"]').val()); - formData.append('create_merge_request', form.find('.js-create-merge-request').val()); - formData.append('commit_message', form.find('.js-commit-message').val()); - }); - }, - // Override behavior of adding error underneath preview - error: function(file, errorMessage) { - var stripped; - stripped = $("<div/>").html(errorMessage).text(); - $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show(); +export default class BlobFileDropzone { + constructor(form, method) { + const formDropzone = form.find('.dropzone'); + Dropzone.autoDiscover = false; + + const dropzone = formDropzone.dropzone({ + autoDiscover: false, + autoProcessQueue: false, + url: form.attr('action'), + // Rails uses a hidden input field for PUT + // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails + method: method, + clickable: true, + uploadMultiple: false, + paramName: 'file', + maxFilesize: gon.max_file_size || 10, + parallelUploads: 1, + maxFiles: 1, + addRemoveLinks: true, + previewsContainer: '.dropzone-previews', + headers: { + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content'), + }, + init: function () { + this.on('addedfile', function () { + $('.dropzone-alerts').html('').hide(); + }); + this.on('success', function (header, response) { + window.location.href = response.filePath; + }); + this.on('maxfilesexceeded', function (file) { this.removeFile(file); - } - }); - submitButton = form.find('#submit-all')[0]; - submitButton.addEventListener('click', function(e) { - e.preventDefault(); - e.stopPropagation(); - if (dropzone[0].dropzone.getQueuedFiles().length === 0) { - alert("Please select a file"); - } - dropzone[0].dropzone.processQueue(); - return false; - }); - } + }); + this.on('sending', function (file, xhr, formData) { + formData.append('target_branch', form.find('input[name="target_branch"]').val()); + formData.append('create_merge_request', form.find('.js-create-merge-request').val()); + formData.append('commit_message', form.find('.js-commit-message').val()); + }); + }, + // Override behavior of adding error underneath preview + error: function (file, errorMessage) { + const stripped = $('<div/>').html(errorMessage).text(); + $('.dropzone-alerts').html(`Error uploading file: "${stripped}"`).show(); + this.removeFile(file); + }, + }); - return BlobFileDropzone; - })(); -}).call(window); + const submitButton = form.find('#submit-all')[0]; + submitButton.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + if (dropzone[0].dropzone.getQueuedFiles().length === 0) { + // eslint-disable-next-line no-alert + alert('Please select a file'); + } + dropzone[0].dropzone.processQueue(); + return false; + }); + } +} diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js deleted file mode 100644 index de20eab9cd1..00000000000 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params */ -/* global Api */ - -require('./template_selector'); - -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.BlobGitignoreSelector = (function(superClass) { - extend(BlobGitignoreSelector, superClass); - - function BlobGitignoreSelector() { - return BlobGitignoreSelector.__super__.constructor.apply(this, arguments); - } - - BlobGitignoreSelector.prototype.requestFile = function(query) { - return Api.gitignoreText(query.name, this.requestFileSuccess.bind(this)); - }; - - return BlobGitignoreSelector; - })(gl.TemplateSelector); -}).call(window); diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js deleted file mode 100644 index 43e5c0a5641..00000000000 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js +++ /dev/null @@ -1,26 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, max-len */ -/* global BlobGitignoreSelector */ - -(function() { - this.BlobGitignoreSelectors = (function() { - function BlobGitignoreSelectors(opts) { - var ref; - this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitignore-selector'), this.editor = opts.editor; - this.$dropdowns.each((function(_this) { - return function(i, dropdown) { - var $dropdown; - $dropdown = $(dropdown); - return new BlobGitignoreSelector({ - pattern: /(.gitignore)/, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-gitignore-selector-wrap'), - dropdown: $dropdown, - editor: _this.editor - }); - }; - })(this)); - } - - return BlobGitignoreSelectors; - })(); -}).call(window); diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js deleted file mode 100644 index b582052a76e..00000000000 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle */ -/* global Api */ - -require('./template_selector'); - -(function() { - var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, - hasProp = {}.hasOwnProperty; - - this.BlobLicenseSelector = (function(superClass) { - extend(BlobLicenseSelector, superClass); - - function BlobLicenseSelector() { - return BlobLicenseSelector.__super__.constructor.apply(this, arguments); - } - - BlobLicenseSelector.prototype.requestFile = function(query) { - var data; - data = { - project: this.dropdown.data('project'), - fullname: this.dropdown.data('fullname') - }; - return Api.licenseText(query.id, data, this.requestFileSuccess.bind(this)); - }; - - return BlobLicenseSelector; - })(gl.TemplateSelector); -}).call(window); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js deleted file mode 100644 index c5067b0feae..00000000000 --- a/app/assets/javascripts/blob/blob_license_selectors.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable no-unused-vars, no-param-reassign */ -/* global BlobLicenseSelector */ - -((global) => { - class BlobLicenseSelectors { - constructor({ $dropdowns, editor }) { - this.$dropdowns = $('.js-license-selector'); - this.editor = editor; - this.$dropdowns.each((i, dropdown) => { - const $dropdown = $(dropdown); - return new BlobLicenseSelector({ - editor, - pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, - data: $dropdown.data('data'), - wrapper: $dropdown.closest('.js-license-selector-wrap'), - dropdown: $dropdown, - }); - }); - } - } - - global.BlobLicenseSelectors = BlobLicenseSelectors; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js new file mode 100644 index 00000000000..3062cd51ee3 --- /dev/null +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -0,0 +1,241 @@ +/* eslint-disable class-methods-use-this */ +/* global Flash */ + +import FileTemplateTypeSelector from './template_selectors/type_selector'; +import BlobCiYamlSelector from './template_selectors/ci_yaml_selector'; +import DockerfileSelector from './template_selectors/dockerfile_selector'; +import GitignoreSelector from './template_selectors/gitignore_selector'; +import LicenseSelector from './template_selectors/license_selector'; + +export default class FileTemplateMediator { + constructor({ editor, currentAction }) { + this.editor = editor; + this.currentAction = currentAction; + + this.initTemplateSelectors(); + this.initTemplateTypeSelector(); + this.initDomElements(); + this.initDropdowns(); + this.initPageEvents(); + } + + initTemplateSelectors() { + // Order dictates template type dropdown item order + this.templateSelectors = [ + GitignoreSelector, + BlobCiYamlSelector, + DockerfileSelector, + LicenseSelector, + ].map(TemplateSelectorClass => new TemplateSelectorClass({ mediator: this })); + } + + initTemplateTypeSelector() { + this.typeSelector = new FileTemplateTypeSelector({ + mediator: this, + dropdownData: this.templateSelectors + .map((templateSelector) => { + const cfg = templateSelector.config; + + return { + name: cfg.name, + key: cfg.key, + }; + }), + }); + } + + initDomElements() { + const $templatesMenu = $('.template-selectors-menu'); + const $undoMenu = $templatesMenu.find('.template-selectors-undo-menu'); + const $fileEditor = $('.file-editor'); + + this.$templatesMenu = $templatesMenu; + this.$undoMenu = $undoMenu; + this.$undoBtn = $undoMenu.find('button'); + this.$templateSelectors = $templatesMenu.find('.template-selector-dropdowns-wrap'); + this.$filenameInput = $fileEditor.find('.js-file-path-name-input'); + this.$fileContent = $fileEditor.find('#file-content'); + this.$commitForm = $fileEditor.find('form'); + this.$navLinks = $fileEditor.find('.nav-links'); + } + + initDropdowns() { + if (this.currentAction === 'create') { + this.typeSelector.show(); + } else { + this.hideTemplateSelectorMenu(); + } + + this.displayMatchedTemplateSelector(); + } + + initPageEvents() { + this.listenForFilenameInput(); + this.prepFileContentForSubmit(); + this.listenForPreviewMode(); + } + + listenForFilenameInput() { + this.$filenameInput.on('keyup blur', () => { + this.displayMatchedTemplateSelector(); + }); + } + + prepFileContentForSubmit() { + this.$commitForm.submit(() => { + this.$fileContent.val(this.editor.getValue()); + }); + } + + listenForPreviewMode() { + this.$navLinks.on('click', 'a', (e) => { + const urlPieces = e.target.href.split('#'); + const hash = urlPieces[1]; + if (hash === 'preview') { + this.hideTemplateSelectorMenu(); + } else if (hash === 'editor') { + this.showTemplateSelectorMenu(); + } + }); + } + + selectTemplateType(item, el, e) { + if (e) { + e.preventDefault(); + } + + this.templateSelectors.forEach((selector) => { + if (selector.config.key === item.key) { + selector.show(); + } else { + selector.hide(); + } + }); + + this.typeSelector.setToggleText(item.name); + + this.cacheToggleText(); + } + + selectTemplateFile(selector, query, data) { + selector.renderLoading(); + // in case undo menu is already already there + this.destroyUndoMenu(); + this.fetchFileTemplate(selector.config.endpoint, query, data) + .then((file) => { + this.showUndoMenu(); + this.setEditorContent(file); + this.setFilename(selector.config.name); + selector.renderLoaded(); + }) + .catch(err => new Flash(`An error occurred while fetching the template: ${err}`)); + } + + displayMatchedTemplateSelector() { + const currentInput = this.getFilename(); + this.templateSelectors.forEach((selector) => { + const match = selector.config.pattern.test(currentInput); + + if (match) { + this.typeSelector.show(); + this.selectTemplateType(selector.config); + this.showTemplateSelectorMenu(); + } + }); + } + + fetchFileTemplate(apiCall, query, data) { + return new Promise((resolve) => { + const resolveFile = file => resolve(file); + + if (!data) { + apiCall(query, resolveFile); + } else { + apiCall(query, data, resolveFile); + } + }); + } + + setEditorContent(file) { + if (!file && file !== '') return; + + const newValue = file.content || file; + + this.editor.setValue(newValue, 1); + + this.editor.focus(); + + this.editor.navigateFileStart(); + } + + findTemplateSelectorByKey(key) { + return this.templateSelectors.find(selector => selector.config.key === key); + } + + showUndoMenu() { + this.$undoMenu.removeClass('hidden'); + + this.$undoBtn.on('click', () => { + this.restoreFromCache(); + this.destroyUndoMenu(); + }); + } + + destroyUndoMenu() { + this.cacheFileContents(); + this.cacheToggleText(); + this.$undoMenu.addClass('hidden'); + this.$undoBtn.off('click'); + } + + hideTemplateSelectorMenu() { + this.$templatesMenu.hide(); + } + + showTemplateSelectorMenu() { + this.$templatesMenu.show(); + } + + cacheToggleText() { + this.cachedToggleText = this.getTemplateSelectorToggleText(); + } + + cacheFileContents() { + this.cachedContent = this.editor.getValue(); + this.cachedFilename = this.getFilename(); + } + + restoreFromCache() { + this.setEditorContent(this.cachedContent); + this.setFilename(this.cachedFilename); + this.setTemplateSelectorToggleText(); + } + + getTemplateSelectorToggleText() { + return this.$templateSelectors + .find('.js-template-selector-wrap:visible .dropdown-toggle-text') + .text(); + } + + setTemplateSelectorToggleText() { + return this.$templateSelectors + .find('.js-template-selector-wrap:visible .dropdown-toggle-text') + .text(this.cachedToggleText); + } + + getTypeSelectorToggleText() { + return this.typeSelector.getToggleText(); + } + + getFilename() { + return this.$filenameInput.val(); + } + + setFilename(name) { + this.$filenameInput.val(name); + } + + getSelected() { + return this.templateSelectors.find(selector => selector.selected); + } +} diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js new file mode 100644 index 00000000000..31dd45fac89 --- /dev/null +++ b/app/assets/javascripts/blob/file_template_selector.js @@ -0,0 +1,60 @@ +/* global Api */ + +export default class FileTemplateSelector { + constructor(mediator) { + this.mediator = mediator; + this.$dropdown = null; + this.$wrapper = null; + } + + init() { + const cfg = this.config; + + this.$dropdown = $(cfg.dropdown); + this.$wrapper = $(cfg.wrapper); + this.$loadingIcon = this.$wrapper.find('.fa-chevron-down'); + this.$dropdownToggleText = this.$wrapper.find('.dropdown-toggle-text'); + + this.initDropdown(); + } + + show() { + if (this.$dropdown === null) { + this.init(); + } + + this.$wrapper.removeClass('hidden'); + } + + hide() { + if (this.$dropdown !== null) { + this.$wrapper.addClass('hidden'); + } + } + + getToggleText() { + return this.$dropdownToggleText.text(); + } + + setToggleText(text) { + this.$dropdownToggleText.text(text); + } + + renderLoading() { + this.$loadingIcon + .addClass('fa-spinner fa-spin') + .removeClass('fa-chevron-down'); + } + + renderLoaded() { + this.$loadingIcon + .addClass('fa-chevron-down') + .removeClass('fa-spinner fa-spin'); + } + + reportSelection(query, el, e, data) { + e.preventDefault(); + return this.mediator.selectTemplateFile(this, query, data); + } +} + diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js new file mode 100644 index 00000000000..9b8bfbfc8c0 --- /dev/null +++ b/app/assets/javascripts/blob/notebook/index.js @@ -0,0 +1,85 @@ +/* eslint-disable no-new */ +import Vue from 'vue'; +import VueResource from 'vue-resource'; +import NotebookLab from 'vendor/notebooklab'; + +Vue.use(VueResource); +Vue.use(NotebookLab); + +export default () => { + const el = document.getElementById('js-notebook-viewer'); + + new Vue({ + el, + data() { + return { + error: false, + loadError: false, + loading: true, + json: {}, + }; + }, + template: ` + <div class="container-fluid md prepend-top-default append-bottom-default"> + <div + class="text-center loading" + v-if="loading && !error"> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" + aria-label="iPython notebook loading"> + </i> + </div> + <notebook-lab + v-if="!loading && !error" + :notebook="json" + code-css-class="code white" /> + <p + class="text-center" + v-if="error"> + <span v-if="loadError"> + An error occured whilst loading the file. Please try again later. + </span> + <span v-else> + An error occured whilst parsing the file. + </span> + </p> + </div> + `, + methods: { + loadFile() { + this.$http.get(el.dataset.endpoint) + .then((res) => { + this.json = res.json(); + this.loading = false; + }) + .catch((e) => { + if (e.status) { + this.loadError = true; + } + + this.error = true; + }); + }, + }, + mounted() { + if (gon.katex_css_url) { + const katexStyles = document.createElement('link'); + katexStyles.setAttribute('rel', 'stylesheet'); + katexStyles.setAttribute('href', gon.katex_css_url); + document.head.appendChild(katexStyles); + } + + if (gon.katex_js_url) { + const katexScript = document.createElement('script'); + katexScript.addEventListener('load', () => { + this.loadFile(); + }); + katexScript.setAttribute('src', gon.katex_js_url); + document.head.appendChild(katexScript); + } else { + this.loadFile(); + } + }, + }); +}; diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js new file mode 100644 index 00000000000..b7a0a195a92 --- /dev/null +++ b/app/assets/javascripts/blob/notebook_viewer.js @@ -0,0 +1,3 @@ +import renderNotebook from './notebook'; + +document.addEventListener('DOMContentLoaded', renderNotebook); diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js new file mode 100644 index 00000000000..5b79717d1e1 --- /dev/null +++ b/app/assets/javascripts/blob/pdf/index.js @@ -0,0 +1,62 @@ +/* eslint-disable no-new */ +import Vue from 'vue'; +import PDFLab from 'vendor/pdflab'; +import workerSrc from 'vendor/pdf.worker'; + +Vue.use(PDFLab, { + workerSrc, +}); + +export default () => { + const el = document.getElementById('js-pdf-viewer'); + + new Vue({ + el, + data() { + return { + error: false, + loadError: false, + loading: true, + pdf: el.dataset.endpoint, + }; + }, + methods: { + onLoad() { + this.loading = false; + }, + onError(error) { + this.loading = false; + this.loadError = true; + this.error = error; + }, + }, + template: ` + <div class="container-fluid md prepend-top-default append-bottom-default"> + <div + class="text-center loading" + v-if="loading && !error"> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" + aria-label="PDF loading"> + </i> + </div> + <pdf-lab + v-if="!loadError" + :pdf="pdf" + @pdflabload="onLoad" + @pdflaberror="onError" /> + <p + class="text-center" + v-if="error"> + <span v-if="loadError"> + An error occured whilst loading the file. Please try again later. + </span> + <span v-else> + An error occured whilst decoding the file. + </span> + </p> + </div> + `, + }); +}; diff --git a/app/assets/javascripts/blob/pdf_viewer.js b/app/assets/javascripts/blob/pdf_viewer.js new file mode 100644 index 00000000000..91abe9dd699 --- /dev/null +++ b/app/assets/javascripts/blob/pdf_viewer.js @@ -0,0 +1,3 @@ +import renderPDF from './pdf'; + +document.addEventListener('DOMContentLoaded', renderPDF); diff --git a/app/assets/javascripts/blob/sketch/index.js b/app/assets/javascripts/blob/sketch/index.js new file mode 100644 index 00000000000..0799991aa40 --- /dev/null +++ b/app/assets/javascripts/blob/sketch/index.js @@ -0,0 +1,73 @@ +import JSZip from 'jszip'; +import JSZipUtils from 'jszip-utils'; + +export default class SketchLoader { + constructor(container) { + this.container = container; + this.loadingIcon = this.container.querySelector('.js-loading-icon'); + + this.load(); + } + + load() { + return this.getZipFile() + .then(data => JSZip.loadAsync(data)) + .then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array')) + .then((content) => { + const url = window.URL || window.webkitURL; + const blob = new Blob([new Uint8Array(content)], { + type: 'image/png', + }); + const previewUrl = url.createObjectURL(blob); + + this.render(previewUrl); + }) + .catch(this.error.bind(this)); + } + + getZipFile() { + return new JSZip.external.Promise((resolve, reject) => { + JSZipUtils.getBinaryContent(this.container.dataset.endpoint, (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); + } + + render(previewUrl) { + const previewLink = document.createElement('a'); + const previewImage = document.createElement('img'); + + previewLink.href = previewUrl; + previewLink.target = '_blank'; + previewImage.src = previewUrl; + previewImage.className = 'img-responsive'; + + previewLink.appendChild(previewImage); + this.container.appendChild(previewLink); + + this.removeLoadingIcon(); + } + + error() { + const errorMsg = document.createElement('p'); + + errorMsg.className = 'prepend-top-default append-bottom-default text-center'; + errorMsg.textContent = ` + Cannot show preview. For previews on sketch files, they must have the file format + introduced by Sketch version 43 and above. + `; + this.container.appendChild(errorMsg); + + this.removeLoadingIcon(); + } + + removeLoadingIcon() { + if (this.loadingIcon) { + this.loadingIcon.remove(); + } + } +} diff --git a/app/assets/javascripts/blob/sketch_viewer.js b/app/assets/javascripts/blob/sketch_viewer.js new file mode 100644 index 00000000000..0640dd26855 --- /dev/null +++ b/app/assets/javascripts/blob/sketch_viewer.js @@ -0,0 +1,8 @@ +/* eslint-disable no-new */ +import SketchLoader from './sketch'; + +document.addEventListener('DOMContentLoaded', () => { + const el = document.getElementById('js-sketch-viewer'); + + new SketchLoader(el); +}); diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 7e03ec3b391..d7c1c32efbd 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -1,101 +1,92 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, no-param-reassign, max-len */ - -((global) => { - class TemplateSelector { - constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) { - this.onClick = this.onClick.bind(this); - this.dropdown = dropdown; - this.data = data; - this.pattern = pattern; - this.wrapper = wrapper; - this.editor = editor; - this.fileEndpoint = fileEndpoint; - this.$input = $input || $('#file_name'); - this.dropdownIcon = $('.fa-chevron-down', this.dropdown); - this.buildDropdown(); - this.bindEvents(); - this.onFilenameUpdate(); - - this.autosizeUpdateEvent = document.createEvent('Event'); - this.autosizeUpdateEvent.initEvent('autosize:update', true, false); - } +/* eslint-disable class-methods-use-this, no-unused-vars */ - buildDropdown() { - return this.dropdown.glDropdown({ - data: this.data, - filterable: true, - selectable: true, - toggleLabel: this.toggleLabel, - search: { - fields: ['name'] - }, - clicked: this.onClick, - text: function(item) { - return item.name; - } - }); - } +export default class TemplateSelector { + constructor({ dropdown, data, pattern, wrapper, editor, $input } = {}) { + this.pattern = pattern; + this.editor = editor; + this.dropdown = dropdown; + this.$dropdownContainer = wrapper; + this.$filenameInput = $input || $('#file_name'); + this.$dropdownIcon = $('.fa-chevron-down', dropdown); - bindEvents() { - return this.$input.on('keyup blur', (e) => this.onFilenameUpdate()); - } + this.initDropdown(dropdown, data); + this.listenForFilenameInput(); + this.renderMatchedDropdown(); + this.initAutosizeUpdateEvent(); + } - toggleLabel(item) { - return item.name; - } + initDropdown(dropdown, data) { + return $(dropdown).glDropdown({ + data, + filterable: true, + selectable: true, + toggleLabel: item => item.name, + search: { + fields: ['name'], + }, + clicked: (item, el, e) => this.fetchFileTemplate(item, el, e), + text: item => item.name, + }); + } - onFilenameUpdate() { - var filenameMatches; - if (!this.$input.length) { - return; - } - filenameMatches = this.pattern.test(this.$input.val().trim()); - if (!filenameMatches) { - this.wrapper.addClass('hidden'); - return; - } - return this.wrapper.removeClass('hidden'); - } + initAutosizeUpdateEvent() { + this.autosizeUpdateEvent = document.createEvent('Event'); + this.autosizeUpdateEvent.initEvent('autosize:update', true, false); + } - onClick(item, el, e) { - e.preventDefault(); - return this.requestFile(item); + listenForFilenameInput() { + return this.$filenameInput.on('keyup blur', e => this.renderMatchedDropdown(e)); + } + + renderMatchedDropdown() { + if (!this.$filenameInput.length) { + return null; } - requestFile(item) { - // This `requestFile` method is an abstract method that should - // be added by all subclasses. + const filenameMatches = this.pattern.test(this.$filenameInput.val().trim()); + + if (!filenameMatches) { + return this.$dropdownContainer.addClass('hidden'); } + return this.$dropdownContainer.removeClass('hidden'); + } - // To be implemented on the extending class - // e.g. - // Api.gitignoreText item.name, @requestFileSuccess.bind(@) - requestFileSuccess(file, { skipFocus } = {}) { - if (!file) return; + fetchFileTemplate(item, el, e) { + e.preventDefault(); + return this.requestFile(item); + } - const oldValue = this.editor.getValue(); - const newValue = file.content; + requestFile(item) { + // This `requestFile` method is an abstract method that should + // be added by all subclasses. + } - this.editor.setValue(newValue, 1); - if (!skipFocus) this.editor.focus(); + // To be implemented on the extending class + // e.g. Api.gitlabCiYml(query.name, file => this.setEditorContent(file)); - if (this.editor instanceof jQuery) { - this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); - } - } + setEditorContent(file, { skipFocus } = {}) { + if (!file) return; - startLoadingSpinner() { - this.dropdownIcon - .addClass('fa-spinner fa-spin') - .removeClass('fa-chevron-down'); - } + const newValue = file.content; - stopLoadingSpinner() { - this.dropdownIcon - .addClass('fa-chevron-down') - .removeClass('fa-spinner fa-spin'); + this.editor.setValue(newValue, 1); + + if (!skipFocus) this.editor.focus(); + + if (this.editor instanceof jQuery) { + this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); } } - global.TemplateSelector = TemplateSelector; -})(window.gl || (window.gl = {})); + startLoadingSpinner() { + this.$dropdownIcon + .addClass('fa-spinner fa-spin') + .removeClass('fa-chevron-down'); + } + + stopLoadingSpinner() { + this.$dropdownIcon + .addClass('fa-chevron-down') + .removeClass('fa-spinner fa-spin'); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js new file mode 100644 index 00000000000..935df07677c --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js @@ -0,0 +1,32 @@ +/* global Api */ + +import FileTemplateSelector from '../file_template_selector'; + +export default class BlobCiYamlSelector extends FileTemplateSelector { + constructor({ mediator }) { + super(mediator); + this.config = { + key: 'gitlab-ci-yaml', + name: '.gitlab-ci.yml', + pattern: /(.gitlab-ci.yml)/, + endpoint: Api.gitlabCiYml, + dropdown: '.js-gitlab-ci-yml-selector', + wrapper: '.js-gitlab-ci-yml-selector-wrap', + }; + } + + initDropdown() { + // maybe move to super class as well + this.$dropdown.glDropdown({ + data: this.$dropdown.data('data'), + filterable: true, + selectable: true, + toggleLabel: item => item.name, + search: { + fields: ['name'], + }, + clicked: (query, el, e) => this.reportSelection(query.name, el, e), + text: item => item.name, + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js new file mode 100644 index 00000000000..b4b4d09c315 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -0,0 +1,32 @@ +/* global Api */ + +import FileTemplateSelector from '../file_template_selector'; + +export default class DockerfileSelector extends FileTemplateSelector { + constructor({ mediator }) { + super(mediator); + this.config = { + key: 'dockerfile', + name: 'Dockerfile', + pattern: /(Dockerfile)/, + endpoint: Api.dockerfileYml, + dropdown: '.js-dockerfile-selector', + wrapper: '.js-dockerfile-selector-wrap', + }; + } + + initDropdown() { + // maybe move to super class as well + this.$dropdown.glDropdown({ + data: this.$dropdown.data('data'), + filterable: true, + selectable: true, + toggleLabel: item => item.name, + search: { + fields: ['name'], + }, + clicked: (query, el, e) => this.reportSelection(query.name, el, e), + text: item => item.name, + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js new file mode 100644 index 00000000000..aefae54ae71 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js @@ -0,0 +1,31 @@ +/* global Api */ + +import FileTemplateSelector from '../file_template_selector'; + +export default class BlobGitignoreSelector extends FileTemplateSelector { + constructor({ mediator }) { + super(mediator); + this.config = { + key: 'gitignore', + name: '.gitignore', + pattern: /(.gitignore)/, + endpoint: Api.gitignoreText, + dropdown: '.js-gitignore-selector', + wrapper: '.js-gitignore-selector-wrap', + }; + } + + initDropdown() { + this.$dropdown.glDropdown({ + data: this.$dropdown.data('data'), + filterable: true, + selectable: true, + toggleLabel: item => item.name, + search: { + fields: ['name'], + }, + clicked: (query, el, e) => this.reportSelection(query.name, el, e), + text: item => item.name, + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js new file mode 100644 index 00000000000..c8abd689ab4 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -0,0 +1,38 @@ +/* global Api */ + +import FileTemplateSelector from '../file_template_selector'; + +export default class BlobLicenseSelector extends FileTemplateSelector { + constructor({ mediator }) { + super(mediator); + this.config = { + key: 'license', + name: 'LICENSE', + pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, + endpoint: Api.licenseText, + dropdown: '.js-license-selector', + wrapper: '.js-license-selector-wrap', + }; + } + + initDropdown() { + this.$dropdown.glDropdown({ + data: this.$dropdown.data('data'), + filterable: true, + selectable: true, + toggleLabel: item => item.name, + search: { + fields: ['name'], + }, + clicked: (query, el, e) => { + const data = { + project: this.$dropdown.data('project'), + fullname: this.$dropdown.data('fullname'), + }; + + this.reportSelection(query.id, el, e, data); + }, + text: item => item.name, + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js new file mode 100644 index 00000000000..56f23ef0568 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/type_selector.js @@ -0,0 +1,25 @@ +import FileTemplateSelector from '../file_template_selector'; + +export default class FileTemplateTypeSelector extends FileTemplateSelector { + constructor({ mediator, dropdownData }) { + super(mediator); + this.mediator = mediator; + this.config = { + dropdown: '.js-template-type-selector', + wrapper: '.js-template-type-selector-wrap', + dropdownData, + }; + } + + initDropdown() { + this.$dropdown.glDropdown({ + data: this.config.dropdownData, + filterable: false, + selectable: true, + toggleLabel: item => item.name, + clicked: (item, el, e) => this.mediator.selectTemplateType(item, el, e), + text: item => item.name, + }); + } + +} diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js new file mode 100644 index 00000000000..1c64ccf536f --- /dev/null +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -0,0 +1,33 @@ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */ +/* global EditBlob */ +/* global NewCommitForm */ + +import EditBlob from './edit_blob'; +import BlobFileDropzone from '../blob/blob_file_dropzone'; + +$(() => { + const editBlobForm = $('.js-edit-blob-form'); + const uploadBlobForm = $('.js-upload-blob-form'); + + if (editBlobForm.length) { + const urlRoot = editBlobForm.data('relative-url-root'); + const assetsPath = editBlobForm.data('assets-prefix'); + const blobLanguage = editBlobForm.data('blob-language'); + const currentAction = $('.js-file-title').data('current-action'); + + new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction); + new NewCommitForm(editBlobForm); + } + + if (uploadBlobForm.length) { + const method = uploadBlobForm.data('method'); + + new BlobFileDropzone(uploadBlobForm, method); + new NewCommitForm(uploadBlobForm); + + window.gl.utils.disableButtonIfEmptyField( + uploadBlobForm.find('.js-commit-message'), + '.btn-upload-file', + ); + } +}); diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js deleted file mode 100644 index 0436bbb0eaf..00000000000 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, max-len */ -/* global EditBlob */ -/* global NewCommitForm */ - -require('./edit_blob'); - -(function() { - $(function() { - var url = $(".js-edit-blob-form").data("relative-url-root"); - url += $(".js-edit-blob-form").data("assets-prefix"); - - var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language')); - new NewCommitForm($('.js-edit-blob-form')); - }); -}).call(window); diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index a1127b9e30e..b37988a674d 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,88 +1,83 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, max-len */ /* global ace */ -/* global BlobGitignoreSelectors */ - -(function() { - var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; - - this.EditBlob = (function() { - function EditBlob(assets_path, ace_mode) { - if (ace_mode == null) { - ace_mode = null; - } - this.editModeLinkClickHandler = bind(this.editModeLinkClickHandler, this); - ace.config.set("modePath", assets_path + "/ace"); - ace.config.loadModule("ace/ext/searchbox"); - this.editor = ace.edit("editor"); - this.editor.focus(); - if (ace_mode) { - this.editor.getSession().setMode("ace/mode/" + ace_mode); - } - $('form').submit((function(_this) { - return function() { - return $("#file-content").val(_this.editor.getValue()); - }; - // Before a form submission, move the content from the Ace editor into the - // submitted textarea - })(this)); - this.initModePanesAndLinks(); - this.initSoftWrap(); - new gl.BlobLicenseSelectors({ - editor: this.editor - }); - new BlobGitignoreSelectors({ - editor: this.editor - }); - new gl.BlobCiYamlSelectors({ - editor: this.editor - }); - new gl.BlobDockerfileSelectors({ - editor: this.editor + +import TemplateSelectorMediator from '../blob/file_template_mediator'; + +export default class EditBlob { + constructor(assetsPath, aceMode, currentAction) { + this.configureAceEditor(aceMode, assetsPath); + this.initModePanesAndLinks(); + this.initSoftWrap(); + this.initFileSelectors(currentAction); + } + + configureAceEditor(aceMode, assetsPath) { + ace.config.set('modePath', `${assetsPath}/ace`); + ace.config.loadModule('ace/ext/searchbox'); + + this.editor = ace.edit('editor'); + + // This prevents warnings re: automatic scrolling being logged + this.editor.$blockScrolling = Infinity; + + this.editor.focus(); + + if (aceMode) { + this.editor.getSession().setMode(`ace/mode/${aceMode}`); + } + } + + initFileSelectors(currentAction) { + this.fileTemplateMediator = new TemplateSelectorMediator({ + currentAction, + editor: this.editor, + }); + } + + initModePanesAndLinks() { + this.$editModePanes = $('.js-edit-mode-pane'); + this.$editModeLinks = $('.js-edit-mode a'); + this.$editModeLinks.on('click', e => this.editModeLinkClickHandler(e)); + } + + editModeLinkClickHandler(e) { + e.preventDefault(); + + const currentLink = $(e.target); + const paneId = currentLink.attr('href'); + const currentPane = this.$editModePanes.filter(paneId); + + this.$editModeLinks.parent().removeClass('active hover'); + + currentLink.parent().addClass('active hover'); + + this.$editModePanes.hide(); + + currentPane.fadeIn(200); + + if (paneId === '#preview') { + this.$toggleButton.hide(); + return $.post(currentLink.data('preview-url'), { + content: this.editor.getValue(), + }, (response) => { + currentPane.empty().append(response); + return currentPane.renderGFM(); }); } - EditBlob.prototype.initModePanesAndLinks = function() { - this.$editModePanes = $(".js-edit-mode-pane"); - this.$editModeLinks = $(".js-edit-mode a"); - return this.$editModeLinks.click(this.editModeLinkClickHandler); - }; - - EditBlob.prototype.editModeLinkClickHandler = function(event) { - var currentLink, currentPane, paneId; - event.preventDefault(); - currentLink = $(event.target); - paneId = currentLink.attr("href"); - currentPane = this.$editModePanes.filter(paneId); - this.$editModeLinks.parent().removeClass("active hover"); - currentLink.parent().addClass("active hover"); - this.$editModePanes.hide(); - currentPane.fadeIn(200); - if (paneId === "#preview") { - this.$toggleButton.hide(); - return $.post(currentLink.data("preview-url"), { - content: this.editor.getValue() - }, function(response) { - currentPane.empty().append(response); - return currentPane.renderGFM(); - }); - } else { - this.$toggleButton.show(); - return this.editor.focus(); - } - }; - - EditBlob.prototype.initSoftWrap = function() { - this.isSoftWrapped = false; - this.$toggleButton = $('.soft-wrap-toggle'); - this.$toggleButton.on('click', this.toggleSoftWrap.bind(this)); - }; - - EditBlob.prototype.toggleSoftWrap = function(e) { - this.isSoftWrapped = !this.isSoftWrapped; - this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped); - this.editor.getSession().setUseWrapMode(this.isSoftWrapped); - }; - - return EditBlob; - })(); -}).call(window); + this.$toggleButton.show(); + + return this.editor.focus(); + } + + initSoftWrap() { + this.isSoftWrapped = false; + this.$toggleButton = $('.soft-wrap-toggle'); + this.$toggleButton.on('click', () => this.toggleSoftWrap()); + } + + toggleSoftWrap() { + this.isSoftWrapped = !this.isSoftWrapped; + this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped); + this.editor.getSession().setUseWrapMode(this.isSoftWrapped); + } +} diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 3874c2819a5..e057ac8df02 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -1,12 +1,11 @@ /* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ -/* global Vue */ /* global BoardService */ +import Vue from 'vue'; +import VueResource from 'vue-resource'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); require('./models/issue'); require('./models/label'); require('./models/list'); @@ -24,6 +23,8 @@ require('./components/new_list_dropdown'); require('./components/modal/index'); require('../vue_shared/vue_resource_interceptor'); +Vue.use(VueResource); + $(() => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; @@ -78,7 +79,7 @@ $(() => { resp.json().forEach((board) => { const list = Store.addList(board); - if (list.type === 'done') { + if (list.type === 'closed') { list.position = Infinity; } }); diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 67c0c419713..93b8960da2e 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var */ -/* global Vue */ /* global Sortable */ - +import Vue from 'vue'; +import boardList from './board_list'; import boardBlankState from './board_blank_state'; require('./board_delete'); @@ -16,7 +16,7 @@ require('./board_list'); gl.issueBoards.Board = Vue.extend({ template: '#js-board-template', components: { - 'board-list': gl.issueBoards.BoardList, + boardList, 'board-delete': gl.issueBoards.BoardDelete, boardBlankState, }, diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js index 52893d4642b..3fc68457961 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js +++ b/app/assets/javascripts/boards/components/board_blank_state.js @@ -1,5 +1,7 @@ /* global ListLabel */ -/* global Cookies */ + +import Cookies from 'js-cookie'; + const Store = gl.issueBoards.BoardsStore; export default { diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js index 4b72090df31..f591134c548 100644 --- a/app/assets/javascripts/boards/components/board_card.js +++ b/app/assets/javascripts/boards/components/board_card.js @@ -1,4 +1,3 @@ -/* global Vue */ require('./issue_card_inner'); const Store = gl.issueBoards.BoardsStore; @@ -51,9 +50,7 @@ export default { this.showDetail = false; }, showIssue(e) { - const targetTagName = e.target.tagName.toLowerCase(); - - if (targetTagName === 'a' || targetTagName === 'button') return; + if (e.target.classList.contains('js-no-trigger')) return; if (this.showDetail) { this.showDetail = false; diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js index 861600424a5..af621cfd57f 100644 --- a/app/assets/javascripts/boards/components/board_delete.js +++ b/app/assets/javascripts/boards/components/board_delete.js @@ -1,5 +1,6 @@ /* eslint-disable comma-dangle, space-before-function-paren, no-alert */ -/* global Vue */ + +import Vue from 'vue'; (() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js index 1330d4ae840..adbd82cb687 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -1,131 +1,197 @@ -/* eslint-disable comma-dangle, space-before-function-paren, max-len */ -/* global Vue */ /* global Sortable */ - import boardNewIssue from './board_new_issue'; import boardCard from './board_card'; +import eventHub from '../eventhub'; -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; +const Store = gl.issueBoards.BoardsStore; - gl.issueBoards.BoardList = Vue.extend({ - template: '#js-board-list-template', - components: { - boardCard, - boardNewIssue, +export default { + name: 'BoardList', + props: { + disabled: { + type: Boolean, + required: true, }, - props: { - disabled: Boolean, - list: Object, - issues: Array, - loading: Boolean, - issueLinkBase: String, - rootPath: String, + list: { + type: Object, + required: true, }, - data () { - return { - scrollOffset: 250, - filters: Store.state.filters, - showCount: false, - showIssueForm: false - }; + issues: { + type: Array, + required: true, }, - watch: { - filters: { - handler () { - this.list.loadingMore = false; - this.$refs.list.scrollTop = 0; - }, - deep: true - }, - issues () { - this.$nextTick(() => { - if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) { - this.list.page += 1; - this.list.getIssues(false); - } + loading: { + type: Boolean, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + }, + data() { + return { + scrollOffset: 250, + filters: Store.state.filters, + showCount: false, + showIssueForm: false, + }; + }, + components: { + boardCard, + boardNewIssue, + }, + methods: { + listHeight() { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight() { + return this.$refs.list.scrollHeight; + }, + scrollTop() { + return this.$refs.list.scrollTop + this.listHeight(); + }, + loadNextPage() { + const getIssues = this.list.nextPage(); - if (this.scrollHeight() > this.listHeight()) { - this.showCount = true; - } else { - this.showCount = false; - } + if (getIssues) { + this.list.loadingMore = true; + getIssues.then(() => { + this.list.loadingMore = false; }); } }, - methods: { - listHeight () { - return this.$refs.list.getBoundingClientRect().height; - }, - scrollHeight () { - return this.$refs.list.scrollHeight; - }, - scrollTop () { - return this.$refs.list.scrollTop + this.listHeight(); + toggleForm() { + this.showIssueForm = !this.showIssueForm; + }, + onScroll() { + if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { + this.loadNextPage(); + } + }, + }, + watch: { + filters: { + handler() { + this.list.loadingMore = false; + this.$refs.list.scrollTop = 0; }, - loadNextPage () { - const getIssues = this.list.nextPage(); + deep: true, + }, + issues() { + this.$nextTick(() => { + if (this.scrollHeight() <= this.listHeight() && + this.list.issuesSize > this.list.issues.length) { + this.list.page += 1; + this.list.getIssues(false); + } - if (getIssues) { - this.list.loadingMore = true; - getIssues.then(() => { - this.list.loadingMore = false; - }); + if (this.scrollHeight() > Math.ceil(this.listHeight())) { + this.showCount = true; + } else { + this.showCount = false; } - }, - toggleForm() { - this.showIssueForm = !this.showIssueForm; - }, - }, - created() { - gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); + }); }, - mounted () { - const options = gl.issueBoards.getBoardSortableDefaultOptions({ - scroll: document.querySelectorAll('.boards-list')[0], - group: 'issues', - disabled: this.disabled, - filter: '.board-list-count, .is-disabled', - dataIdAttr: 'data-issue-id', - onStart: (e) => { - const card = this.$refs.issue[e.oldIndex]; + }, + created() { + eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); + }, + mounted() { + const options = gl.issueBoards.getBoardSortableDefaultOptions({ + scroll: document.querySelectorAll('.boards-list')[0], + group: 'issues', + disabled: this.disabled, + filter: '.board-list-count, .is-disabled', + dataIdAttr: 'data-issue-id', + onStart: (e) => { + const card = this.$refs.issue[e.oldIndex]; - card.showDetail = false; - Store.moving.list = card.list; - Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); + card.showDetail = false; + Store.moving.list = card.list; + Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId); - gl.issueBoards.onStart(); - }, - onAdd: (e) => { - gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); + gl.issueBoards.onStart(); + }, + onAdd: (e) => { + gl.issueBoards.BoardsStore + .moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); - this.$nextTick(() => { - e.item.remove(); - }); - }, - onUpdate: (e) => { - const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); - gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); - }, - onMove(e) { - return !e.related.classList.contains('board-list-count'); - } - }); + this.$nextTick(() => { + e.item.remove(); + }); + }, + onUpdate: (e) => { + const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); + gl.issueBoards.BoardsStore + .moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); + }, + onMove(e) { + return !e.related.classList.contains('board-list-count'); + }, + }); - this.sortable = Sortable.create(this.$refs.list, options); + this.sortable = Sortable.create(this.$refs.list, options); - // Scroll event on list to load more - this.$refs.list.onscroll = () => { - if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) { - this.loadNextPage(); - } - }; - }, - beforeDestroy() { - gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); - }, - }); -})(); + // Scroll event on list to load more + this.$refs.list.addEventListener('scroll', this.onScroll); + }, + beforeDestroy() { + eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); + this.$refs.list.removeEventListener('scroll', this.onScroll); + }, + template: ` + <div class="board-list-component"> + <div + class="board-list-loading text-center" + aria-label="Loading issues" + v-if="loading"> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true"> + </i> + </div> + <board-new-issue + :list="list" + v-if="list.type !== 'closed' && showIssueForm"/> + <ul + class="board-list" + v-show="!loading" + ref="list" + :data-board="list.id" + :class="{ 'is-smaller': showIssueForm }"> + <board-card + v-for="(issue, index) in issues" + ref="issue" + :index="index" + :list="list" + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath" + :disabled="disabled" + :key="issue.id" /> + <li + class="board-list-count text-center" + v-if="showCount" + data-id="-1"> + <i + class="fa fa-spinner fa-spin" + aria-label="Loading more issues" + aria-hidden="true" + v-show="list.loadingMore"> + </i> + <span v-if="list.issues.length === list.issuesSize"> + Showing all issues + </span> + <span v-else> + Showing {{ list.issues.length }} of {{ list.issuesSize }} issues + </span> + </li> + </ul> + </div> + `, +}; diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js index b88f59dd6d4..0fa85b6fe14 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js +++ b/app/assets/javascripts/boards/components/board_new_issue.js @@ -1,4 +1,6 @@ /* global ListIssue */ +import eventHub from '../eventhub'; + const Store = gl.issueBoards.BoardsStore; export default { @@ -49,7 +51,7 @@ export default { }, cancel() { this.title = ''; - gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`); + eventHub.$emit(`hide-issue-form-${this.list.id}`); }, }, mounted() { diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index dfc6eed785c..3c080008244 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -1,10 +1,11 @@ /* eslint-disable comma-dangle, space-before-function-paren, no-new */ -/* global Vue */ /* global IssuableContext */ /* global MilestoneSelect */ /* global LabelsSelect */ /* global Sidebar */ +import Vue from 'vue'; + require('./sidebar/remove_issue'); (() => { diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 69e30cec4c5..a4629b092bf 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -1,4 +1,4 @@ -/* global Vue */ +import Vue from 'vue'; import eventHub from '../eventhub'; (() => { @@ -84,20 +84,20 @@ import eventHub from '../eventhub'; #{{ issue.id }} </span> <a - class="card-assignee has-tooltip" + class="card-assignee has-tooltip js-no-trigger" :href="rootPath + issue.assignee.username" :title="'Assigned to ' + issue.assignee.name" v-if="issue.assignee" data-container="body"> <img - class="avatar avatar-inline s20" + class="avatar avatar-inline s20 js-no-trigger" :src="issue.assignee.avatar" width="20" height="20" :alt="'Avatar for ' + issue.assignee.name" /> </a> <button - class="label color-label has-tooltip" + class="label color-label has-tooltip js-no-trigger" v-for="label in issue.labels" type="button" v-if="showLabel(label)" diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js index e6973c3fd59..823319df6e7 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -1,4 +1,5 @@ -/* global Vue */ +import Vue from 'vue'; + (() => { const ModalStore = gl.issueBoards.ModalStore; diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js index bd394a2318c..b214b5a7199 100644 --- a/app/assets/javascripts/boards/components/modal/filters.js +++ b/app/assets/javascripts/boards/components/modal/filters.js @@ -14,8 +14,10 @@ export default { this.filteredSearch = new FilteredSearchBoards(this.store); this.filteredSearch.removeTokens(); + this.filteredSearch.handleInputPlaceholder(); + this.filteredSearch.toggleClearSearchButton(); }, - beforeDestroy() { + destroyed() { this.filteredSearch.cleanup(); FilteredSearchContainer.container = document; this.store.path = ''; diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 1cbc422c961..887ce373096 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -1,7 +1,8 @@ /* eslint-disable no-new */ -/* global Vue */ /* global Flash */ +import Vue from 'vue'; + require('./lists_dropdown'); (() => { diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index 4240c97617d..91c08cde13a 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -1,5 +1,6 @@ -/* global Vue */ /* global ListIssue */ + +import Vue from 'vue'; import queryData from '../../utils/query_data'; require('./header'); @@ -64,8 +65,15 @@ require('./empty_state'); }, filter: { handler() { - this.page = 1; - this.loadIssues(true); + if (this.$el.tagName) { + this.page = 1; + this.filterLoading = true; + + this.loadIssues(true) + .then(() => { + this.filterLoading = false; + }); + } }, deep: true, }, @@ -139,14 +147,14 @@ require('./empty_state'); :image="blankStateImage" :issue-link-base="issueLinkBase" :root-path="rootPath" - v-if="!loading && showList"></modal-list> + v-if="!loading && showList && !filterLoading"></modal-list> <empty-state v-if="showEmptyState" :image="blankStateImage" :new-issue-path="newIssuePath"></empty-state> <section class="add-issues-list text-center" - v-if="loading"> + v-if="loading || filterLoading"> <div class="add-issues-list-loading"> <i class="fa fa-spinner fa-spin"></i> </div> diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index 3730c1ecaeb..aba56d4aa31 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -1,6 +1,8 @@ -/* global Vue */ /* global ListIssue */ /* global bp */ + +import Vue from 'vue'; + (() => { const ModalStore = gl.issueBoards.ModalStore; diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js index 3c05120a2da..9e9ed46ab8d 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js @@ -1,4 +1,5 @@ -/* global Vue */ +import Vue from 'vue'; + (() => { const ModalStore = gl.issueBoards.ModalStore; diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js index 1cd6ca0ee88..23cb1b13d11 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js +++ b/app/assets/javascripts/boards/components/modal/tabs.js @@ -1,4 +1,5 @@ -/* global Vue */ +import Vue from 'vue'; + (() => { const ModalStore = gl.issueBoards.ModalStore; diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index e74935e1cb0..772ea4c5565 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -1,6 +1,8 @@ /* eslint-disable no-new */ -/* global Vue */ /* global Flash */ + +import Vue from 'vue'; + (() => { const Store = gl.issueBoards.BoardsStore; @@ -46,7 +48,7 @@ template: ` <div class="block list" - v-if="list.type !== 'done'"> + v-if="list.type !== 'closed'"> <button class="btn btn-default btn-block" type="button" diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 101732309ea..1264280284c 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -28,6 +28,8 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager { [].forEach.call(tokens, (el) => { el.parentNode.removeChild(el); }); + + this.filteredSearchInput.value = ''; } updateTokens() { diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js b/app/assets/javascripts/boards/filters/due_date_filters.js index 03425bb145b..70132dbfa6f 100644 --- a/app/assets/javascripts/boards/filters/due_date_filters.js +++ b/app/assets/javascripts/boards/filters/due_date_filters.js @@ -1,6 +1,7 @@ -/* global Vue */ /* global dateFormat */ +import Vue from 'vue'; + Vue.filter('due-date', (value) => { const date = new Date(value); return dateFormat(date, 'mmm d, yyyy', true); diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index ca5e6fa7e9d..d6175069e37 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -1,9 +1,10 @@ /* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */ -/* global Vue */ /* global ListLabel */ /* global ListMilestone */ /* global ListUser */ +import Vue from 'vue'; + class ListIssue { constructor (obj) { this.globalId = obj.id; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index f18ad2a0fac..91e5fb2a666 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -10,7 +10,7 @@ class List { this.position = obj.position; this.title = obj.title; this.type = obj.list_type; - this.preset = ['done', 'blank'].indexOf(this.type) > -1; + this.preset = ['closed', 'blank'].indexOf(this.type) > -1; this.page = 1; this.loading = true; this.loadingMore = false; diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index e54102814d6..db9bced2f89 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -1,5 +1,6 @@ /* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */ -/* global Vue */ + +import Vue from 'vue'; class BoardService { constructor (root, bulkUpdatePath, boardId) { diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 28ecb322df7..bcda70d0638 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -1,7 +1,8 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ -/* global Cookies */ /* global List */ +import Cookies from 'js-cookie'; + (() => { window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -44,7 +45,7 @@ }, shouldAddBlankState () { // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'done')[0]); + return !(this.state.lists.filter(list => list.type !== 'closed')[0]); }, addBlankState () { if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; @@ -97,7 +98,7 @@ issueTo.removeLabel(listFrom.label); } - if (listTo.type === 'done') { + if (listTo.type === 'closed') { issueLists.forEach((list) => { list.removeIssue(issue); }); diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js index 7ee266a831f..9b009483a3c 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -15,6 +15,7 @@ searchTerm: '', loading: false, loadingNewPage: false, + filterLoading: false, page: 1, perPage: 50, filter: { diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js index a9f2d462c31..a92e068ca5a 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js @@ -1,8 +1,10 @@ /* eslint-disable no-param-reassign */ + +import Vue from 'vue'; +import VueResource from 'vue-resource'; import CommitPipelinesTable from './pipelines_table'; -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); +Vue.use(VueResource); /** * Commits View > Pipelines Tab > Pipelines Table. diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index 832c4b1bd2a..4d5a857d705 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -1,10 +1,10 @@ -/* eslint-disable no-new*/ -/* global Flash */ import Vue from 'vue'; import PipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import PipelinesService from '../../vue_pipelines_index/services/pipelines_service'; import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store'; import eventHub from '../../vue_pipelines_index/event_hub'; +import EmptyState from '../../vue_pipelines_index/components/empty_state'; +import ErrorState from '../../vue_pipelines_index/components/error_state'; import '../../lib/utils/common_utils'; import '../../vue_shared/vue_resource_interceptor'; @@ -22,6 +22,8 @@ import '../../vue_shared/vue_resource_interceptor'; export default Vue.component('pipelines-table', { components: { 'pipelines-table-component': PipelinesTableComponent, + 'error-state': ErrorState, + 'empty-state': EmptyState, }, /** @@ -31,17 +33,28 @@ export default Vue.component('pipelines-table', { * @return {Object} */ data() { - const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; const store = new PipelineStore(); return { - endpoint: pipelinesTableData.endpoint, + endpoint: null, + helpPagePath: null, store, state: store.state, isLoading: false, + hasError: false, }; }, + computed: { + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + + shouldRenderEmptyState() { + return !this.state.pipelines.length && !this.isLoading; + }, + }, + /** * When the component is about to be mounted, tell the service to fetch the data * @@ -51,6 +64,8 @@ export default Vue.component('pipelines-table', { * */ beforeMount() { + this.endpoint = this.$el.dataset.endpoint; + this.helpPagePath = this.$el.dataset.helpPagePath; this.service = new PipelinesService(this.endpoint); this.fetchPipelines(); @@ -80,26 +95,25 @@ export default Vue.component('pipelines-table', { this.isLoading = false; }) .catch(() => { + this.hasError = true; this.isLoading = false; - new Flash('An error occurred while fetching the pipelines, please reload the page again.'); }); }, }, template: ` - <div class="pipelines"> + <div class="content-list pipelines"> <div class="realtime-loading" v-if="isLoading"> <i class="fa fa-spinner fa-spin"></i> </div> - <div class="blank-state blank-state-no-icon" - v-if="!isLoading && state.pipelines.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - No pipelines to show - </h2> - </div> + <empty-state + v-if="shouldRenderEmptyState" + :help-page-path="helpPagePath" /> + + <error-state v-if="shouldRenderErrorState" /> - <div class="table-holder pipelines" + <div class="table-holder" v-if="!isLoading && state.pipelines.length > 0"> <pipelines-table-component :pipelines="state.pipelines" diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index fbd0db64ca7..3253eebd9b5 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -1,9 +1,11 @@ // ECMAScript polyfills import 'core-js/fn/array/find'; +import 'core-js/fn/array/from'; import 'core-js/fn/object/assign'; import 'core-js/fn/promise'; import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/from-code-point'; +import 'core-js/fn/symbol'; // Browser polyfills import './polyfills/custom_event'; diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js new file mode 100644 index 00000000000..abe48572347 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js @@ -0,0 +1,17 @@ +export default { + props: { + count: { + type: Number, + required: true, + }, + }, + template: ` + <span v-if="count === 50" class="events-info pull-right"> + <i class="fa fa-warning has-tooltip" + aria-hidden="true" + title="Limited to showing 50 events at most" + data-placement="top"></i> + Showing 50 events + </span> + `, +}; diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js index b83a4c63fad..3f419a96ff9 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ -/* global Vue */ + +import Vue from 'vue'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -13,6 +14,7 @@ <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="mergeRequest in items" class="stage-event-item"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js index cb1687dcc7a..7ffa38edd9e 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ -/* global Vue */ + +import Vue from 'vue'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -13,6 +14,7 @@ <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="issue in items" class="stage-event-item"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js index 42e1bbce744..d736c8b0c28 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js @@ -19,12 +19,7 @@ import iconCommit from '../svg/icon_commit.svg'; <div> <div class="events-description"> {{ stage.description }} - <span v-if="items.length === 50" class="events-info pull-right"> - <i class="fa fa-warning has-tooltip" - title="Limited to showing 50 events at most" - data-placement="top"></i> - Showing 50 events - </span> + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="commit in items" class="stage-event-item"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js index 73f4205b578..698a79ca68c 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ -/* global Vue */ + +import Vue from 'vue'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -13,6 +14,7 @@ <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="issue in items" class="stage-event-item"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js index 501ffb1fac9..e63c41f2a57 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ -/* global Vue */ + +import Vue from 'vue'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; @@ -13,6 +14,7 @@ <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="mergeRequest in items" class="stage-event-item"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js index 8fa63734cf1..d51f7134e25 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js @@ -17,6 +17,7 @@ import iconBranch from '../svg/icon_branch.svg'; <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="build in items" class="stage-event-item item-build-component"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js index 0015249cfaa..17ae3a9ddc1 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js @@ -18,6 +18,7 @@ import iconBranch from '../svg/icon_branch.svg'; <div> <div class="events-description"> {{ stage.description }} + <limit-warning :count="items.length" /> </div> <ul class="stage-event-list"> <li v-for="build in items" class="stage-event-item item-build-component"> diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js index 0d85e1a4678..b4442ea5566 100644 --- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js +++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ -/* global Vue */ + +import Vue from 'vue'; ((global) => { global.cycleAnalytics = global.cycleAnalytics || {}; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index beff293b587..b099b39e58f 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -1,9 +1,9 @@ -/* global Vue */ -/* global Cookies */ /* global Flash */ -window.Vue = require('vue'); -window.Cookies = require('js-cookie'); +import Vue from 'vue'; +import Cookies from 'js-cookie'; +import LimitWarningComponent from './components/limit_warning_component'; + require('./components/stage_code_component'); require('./components/stage_issue_component'); require('./components/stage_plan_component'); @@ -131,5 +131,6 @@ $(() => { }); // Register global components + Vue.component('limit-warning', LimitWarningComponent); Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent); }); diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index cfa60325fcc..88180149715 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -33,11 +33,7 @@ class Diff { handleClickUnfold(e) { const $target = $(e.target); - // current babel config relies on iterators implementation, so we cannot simply do: - // const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); - const ref = this.lineNumbers($target.parent()); - const oldLineNumber = ref[0]; - const newLineNumber = ref[1]; + const [oldLineNumber, newLineNumber] = this.lineNumbers($target.parent()); const offset = newLineNumber - oldLineNumber; const bottom = $target.hasClass('js-unfold-bottom'); let since; @@ -105,10 +101,11 @@ class Diff { } lineNumbers(line) { - if (!line.children().length) { + const children = line.find('.diff-line-num').toArray(); + if (children.length !== 2) { return [0, 0]; } - return line.find('.diff-line-num').map((i, elm) => parseInt($(elm).data('linenumber'), 10)); + return children.map(elm => parseInt($(elm).data('linenumber'), 10) || 0); } highlightSelectedLine() { diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js index d948dff58ec..fc2f20e3bcb 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js @@ -1,6 +1,7 @@ /* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */ /* global CommentsStore */ -const Vue = require('vue'); + +import Vue from 'vue'; (() => { const CommentAndResolveBtn = Vue.extend({ 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 dd7081aefb7..0297add94d5 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -1,4 +1,6 @@ -/* global CommentsStore Cookies notes */ +/* global CommentsStore */ +/* global notes */ + import Vue from 'vue'; import collapseIcon from '../icons/collapse_icon.svg'; diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 283dc330cad..8edc45130fc 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -1,7 +1,8 @@ /* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, space-before-function-paren, no-lonely-if, no-continue, brace-style, max-len, quotes */ /* global DiscussionMixins */ /* global CommentsStore */ -const Vue = require('vue'); + +import Vue from 'vue'; (() => { const JumpToDiscussion = Vue.extend({ diff --git a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js index e86bef47172..8eb0e10b832 100644 --- a/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js +++ b/app/assets/javascripts/diff_notes/components/new_issue_for_discussion.js @@ -1,6 +1,7 @@ -/* global Vue */ /* global CommentsStore */ +import Vue from 'vue'; + (() => { const NewIssueForDiscussion = Vue.extend({ props: { diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index fbd980f0fce..312f38ce241 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -2,7 +2,8 @@ /* global CommentsStore */ /* global ResolveService */ /* global Flash */ -const Vue = require('vue'); + +import Vue from 'vue'; (() => { const ResolveBtn = Vue.extend({ diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js index de9367f2136..27147ac6b5c 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js @@ -1,7 +1,8 @@ /* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */ /* global DiscussionMixins */ /* global CommentsStore */ -const Vue = require('vue'); + +import Vue from 'vue'; ((w) => { w.ResolveCount = Vue.extend({ diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js index 7c5fcd04d2d..a964b7d0c6b 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js @@ -2,7 +2,7 @@ /* global CommentsStore */ /* global ResolveService */ -const Vue = require('vue'); +import Vue from 'vue'; (() => { const ResolveDiscussionBtn = Vue.extend({ diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index 4f6b86a917c..b6b47e2da6f 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -1,8 +1,8 @@ /* eslint-disable func-names, comma-dangle, new-cap, no-new, max-len */ -/* global Vue */ /* global ResolveCount */ -const Vue = require('vue'); +import Vue from 'vue'; + require('./models/discussion'); require('./models/note'); require('./stores/comments'); diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js index dce1a9b58bd..dc43e4b2cc7 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js +++ b/app/assets/javascripts/diff_notes/models/discussion.js @@ -1,7 +1,8 @@ /* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */ -/* global Vue */ /* global NoteModel */ +import Vue from 'vue'; + class DiscussionModel { constructor (discussionId) { this.id = discussionId; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js index 090c454e9e4..bfa4fc9037a 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -2,10 +2,13 @@ /* global Flash */ /* global CommentsStore */ -const Vue = window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); +import Vue from 'vue'; +import VueResource from 'vue-resource'; + require('../../vue_shared/vue_resource_interceptor'); +Vue.use(VueResource); + (() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js index 69c4d7a8434..e6cbda56c91 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js +++ b/app/assets/javascripts/diff_notes/stores/comments.js @@ -1,7 +1,8 @@ /* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ -/* global Vue */ /* global DiscussionModel */ +import Vue from 'vue'; + ((w) => { w.CommentsStore = { state: {}, diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 4dc82bee6c3..2e7c9bc2689 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -33,6 +33,8 @@ /* global ProjectShow */ /* global Labels */ /* global Shortcuts */ +/* global Sidebar */ + import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; @@ -41,10 +43,10 @@ import GroupsList from './groups_list'; import ProjectsList from './projects_list'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; +import UserCallout from './user_callout'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; const ShortcutsBlob = require('./shortcuts_blob'); -const UserCallout = require('./user_callout'); (function() { var Dispatcher; @@ -119,6 +121,7 @@ const UserCallout = require('./user_callout'); case 'groups:milestones:show': case 'dashboard:milestones:show': new Milestone(); + new Sidebar(); break; case 'dashboard:todos:index': new gl.Todos(); diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js index 9b40a3f20a4..7f7d93f3e27 100644 --- a/app/assets/javascripts/droplab/droplab_filter.js +++ b/app/assets/javascripts/droplab/droplab_filter.js @@ -56,10 +56,12 @@ require('../window')(function(w){ this.hookInput = hookInput; this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper); + this.hookInput.trigger.addEventListener('mousedown.dl', this.keydownWrapper); }, destroy: function destroy(){ this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper); + this.hookInput.trigger.removeEventListener('mousedown.dl', this.keydownWrapper); } }; }); diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js index 51aab8460f6..0518422e475 100644 --- a/app/assets/javascripts/environments/components/environment.js +++ b/app/assets/javascripts/environments/components/environment.js @@ -24,6 +24,7 @@ export default Vue.component('environment-component', { state: store.state, visibility: 'available', isLoading: false, + isLoadingFolderContent: false, cssContainerClass: environmentsData.cssClass, endpoint: environmentsData.environmentsDataEndpoint, canCreateDeployment: environmentsData.canCreateDeployment, @@ -68,15 +69,21 @@ export default Vue.component('environment-component', { this.fetchEnvironments(); eventHub.$on('refreshEnvironments', this.fetchEnvironments); + eventHub.$on('toggleFolder', this.toggleFolder); }, beforeDestroyed() { eventHub.$off('refreshEnvironments'); + eventHub.$off('toggleFolder'); }, methods: { - toggleRow(model) { - return this.store.toggleFolder(model.name); + toggleFolder(folder, folderUrl) { + this.store.toggleFolder(folder); + + if (!folder.isOpen) { + this.fetchChildEnvironments(folder, folderUrl); + } }, /** @@ -117,6 +124,21 @@ export default Vue.component('environment-component', { new Flash('An error occurred while fetching the environments.'); }); }, + + fetchChildEnvironments(folder, folderUrl) { + this.isLoadingFolderContent = true; + + this.service.getFolderContent(folderUrl) + .then(resp => resp.json()) + .then((response) => { + this.store.setfolderContent(folder, response.environments); + this.isLoadingFolderContent = false; + }) + .catch(() => { + this.isLoadingFolderContent = false; + new Flash('An error occurred while fetching the environments.'); + }); + }, }, template: ` @@ -179,7 +201,8 @@ export default Vue.component('environment-component', { :environments="state.environments" :can-create-deployment="canCreateDeploymentParsed" :can-read-environment="canReadEnvironmentParsed" - :service="service"/> + :service="service" + :is-loading-folder-content="isLoadingFolderContent" /> </div> <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js index 455a8819549..4bb7920bb5e 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js +++ b/app/assets/javascripts/environments/components/environment_actions.js @@ -25,6 +25,12 @@ export default { }; }, + computed: { + title() { + return 'Deploy to...'; + }, + }, + methods: { onClickAction(endpoint) { this.isLoading = true; @@ -39,25 +45,45 @@ export default { new Flash('An error occured while making the request.'); }); }, + + isActionDisabled(action) { + if (action.playable === undefined) { + return false; + } + + return !action.playable; + }, }, template: ` <div class="btn-group" role="group"> <button - class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" + type="button" + class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip" + data-container="body" data-toggle="dropdown" + :title="title" + :aria-label="title" :disabled="isLoading"> <span> <span v-html="playIconSvg"></span> - <i class="fa fa-caret-down" aria-hidden="true"></i> - <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> + <i + class="fa fa-caret-down" + aria-hidden="true"/> + <i + v-if="isLoading" + class="fa fa-spinner fa-spin" + aria-hidden="true"/> </span> <ul class="dropdown-menu dropdown-menu-align-right"> <li v-for="action in actions"> <button + type="button" + class="js-manual-action-link no-btn btn" @click="onClickAction(action.play_path)" - class="js-manual-action-link no-btn"> + :class="{ 'disabled': isActionDisabled(action) }" + :disabled="isActionDisabled(action)"> ${playIconSvg} <span> {{action.name}} diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js index b4f9eb357fd..d79b916c360 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js +++ b/app/assets/javascripts/environments/components/environment_external_url.js @@ -9,13 +9,21 @@ export default { }, }, + computed: { + title() { + return 'Open'; + }, + }, + template: ` <a - class="btn external_url" + class="btn external-url has-tooltip" + data-container="body" :href="externalUrl" target="_blank" - rel="noopener noreferrer" - title="Environment external URL"> + rel="noopener noreferrer nofollow" + :title="title" + :aria-label="title"> <i class="fa fa-external-link" aria-hidden="true"></i> </a> `, diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js index 66ed10e19d1..d9b49287dec 100644 --- a/app/assets/javascripts/environments/components/environment_item.js +++ b/app/assets/javascripts/environments/components/environment_item.js @@ -5,7 +5,9 @@ import ExternalUrlComponent from './environment_external_url'; import StopComponent from './environment_stop'; import RollbackComponent from './environment_rollback'; import TerminalButtonComponent from './environment_terminal_button'; +import MonitoringButtonComponent from './environment_monitoring'; import CommitComponent from '../../vue_shared/components/commit'; +import eventHub from '../event_hub'; /** * Envrionment Item Component @@ -22,6 +24,7 @@ export default { 'stop-component': StopComponent, 'rollback-component': RollbackComponent, 'terminal-button-component': TerminalButtonComponent, + 'monitoring-button-component': MonitoringButtonComponent, }, props: { @@ -139,6 +142,7 @@ export default { const parsedAction = { name: gl.text.humanize(action.name), play_path: action.play_path, + playable: action.playable, }; return parsedAction; }); @@ -392,6 +396,14 @@ export default { return ''; }, + monitoringUrl() { + if (this.model && this.model.metrics_path) { + return this.model.metrics_path; + } + + return ''; + }, + /** * Constructs folder URL based on the current location and the folder id. * @@ -400,7 +412,6 @@ export default { folderUrl() { return `${window.location.pathname}/folders/${this.model.folderName}`; }, - }, /** @@ -418,15 +429,37 @@ export default { return true; }, + methods: { + onClickFolder() { + eventHub.$emit('toggleFolder', this.model, this.folderUrl); + }, + }, + template: ` - <tr> + <tr :class="{ 'js-child-row': model.isChildren }"> <td> <a v-if="!model.isFolder" class="environment-name" + :class="{ 'prepend-left-default': model.isChildren }" :href="environmentPath"> {{model.name}} </a> - <a v-else class="folder-name" :href="folderUrl"> + <span v-else + class="folder-name" + @click="onClickFolder" + role="button"> + + <span class="folder-icon"> + <i + v-show="model.isOpen" + class="fa fa-caret-down" + aria-hidden="true" /> + <i + v-show="!model.isOpen" + class="fa fa-caret-right" + aria-hidden="true"/> + </span> + <span class="folder-icon"> <i class="fa fa-folder" aria-hidden="true"></i> </span> @@ -438,7 +471,7 @@ export default { <span class="badge"> {{model.size}} </span> - </a> + </span> </td> <td class="deployment-column"> @@ -496,13 +529,16 @@ export default { <external-url-component v-if="externalURL && canReadEnvironment" :external-url="externalURL"/> - <stop-component v-if="hasStopAction && canCreateDeployment" - :stop-url="model.stop_path" - :service="service"/> + <monitoring-button-component v-if="monitoringUrl && canReadEnvironment" + :monitoring-url="monitoringUrl"/> <terminal-button-component v-if="model && model.terminal_path" :terminal-path="model.terminal_path"/> + <stop-component v-if="hasStopAction && canCreateDeployment" + :stop-url="model.stop_path" + :service="service"/> + <rollback-component v-if="canRetry && canCreateDeployment" :is-last-deployment="isLastDeployment" :retry-url="retryUrl" diff --git a/app/assets/javascripts/environments/components/environment_monitoring.js b/app/assets/javascripts/environments/components/environment_monitoring.js new file mode 100644 index 00000000000..064e2fc7434 --- /dev/null +++ b/app/assets/javascripts/environments/components/environment_monitoring.js @@ -0,0 +1,31 @@ +/** + * Renders the Monitoring (Metrics) link in environments table. + */ +export default { + props: { + monitoringUrl: { + type: String, + default: '', + required: true, + }, + }, + + computed: { + title() { + return 'Monitoring'; + }, + }, + + template: ` + <a + class="btn monitoring-url has-tooltip" + data-container="body" + :href="monitoringUrl" + target="_blank" + rel="noopener noreferrer nofollow" + :title="title" + :aria-label="title"> + <i class="fa fa-area-chart" aria-hidden="true"></i> + </a> + `, +}; diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js index 5404d647745..47102692024 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js +++ b/app/assets/javascripts/environments/components/environment_stop.js @@ -25,6 +25,12 @@ export default { }; }, + computed: { + title() { + return 'Stop'; + }, + }, + methods: { onClick() { if (confirm('Are you sure you want to stop this environment?')) { @@ -45,10 +51,12 @@ export default { template: ` <button type="button" - class="btn stop-env-link" + class="btn stop-env-link has-tooltip" + data-container="body" @click="onClick" :disabled="isLoading" - title="Stop Environment"> + :title="title" + :aria-label="title"> <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i> <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> </button> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js index 66a71faa02f..092a50a0d6f 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js @@ -14,12 +14,22 @@ export default { }, data() { - return { terminalIconSvg }; + return { + terminalIconSvg, + }; + }, + + computed: { + title() { + return 'Terminal'; + }, }, template: ` - <a class="btn terminal-button" - title="Open web terminal" + <a class="btn terminal-button has-tooltip" + data-container="body" + :title="title" + :aria-label="title" :href="terminalPath"> ${terminalIconSvg} </a> diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js index 338dff40bc9..5e6af3a1d45 100644 --- a/app/assets/javascripts/environments/components/environments_table.js +++ b/app/assets/javascripts/environments/components/environments_table.js @@ -31,6 +31,18 @@ export default { type: Object, required: true, }, + + isLoadingFolderContent: { + type: Boolean, + required: false, + default: false, + }, + }, + + methods: { + folderUrl(model) { + return `${window.location.pathname}/folders/${model.folderName}`; + }, }, template: ` @@ -53,6 +65,31 @@ export default { :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" :service="service"></tr> + + <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0"> + <tr v-if="isLoadingFolderContent"> + <td colspan="6" class="text-center"> + <i class="fa fa-spin fa-spinner fa-2x" aria-hidden="true"/> + </td> + </tr> + + <template v-else> + <tr is="environment-item" + v-for="children in model.children" + :model="children" + :can-create-deployment="canCreateDeployment" + :can-read-environment="canReadEnvironment" + :service="service"></tr> + + <tr> + <td colspan="6" class="text-center"> + <a :href="folderUrl(model)" class="btn btn-default"> + Show all + </a> + </td> + </tr> + </template> + </template> </template> </tbody> </table> diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js index 07040bf0d73..8adb53ea86d 100644 --- a/app/assets/javascripts/environments/services/environments_service.js +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -7,6 +7,7 @@ Vue.use(VueResource); export default class EnvironmentsService { constructor(endpoint) { this.environments = Vue.resource(endpoint); + this.folderResults = 3; } get(scope, page) { @@ -16,4 +17,8 @@ export default class EnvironmentsService { postAction(endpoint) { return Vue.http.post(endpoint, {}, { emulateJSON: true }); } + + getFolderContent(folderUrl) { + return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`); + } } diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 3c3084f3b78..158e7922e3c 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -38,7 +38,12 @@ export default class EnvironmentsStore { let filtered = {}; if (env.size > 1) { - filtered = Object.assign({}, env, { isFolder: true, folderName: env.name }); + filtered = Object.assign({}, env, { + isFolder: true, + folderName: env.name, + isOpen: false, + children: [], + }); } if (env.latest) { @@ -85,4 +90,67 @@ export default class EnvironmentsStore { this.state.stoppedCounter = count; return count; } + + /** + * Toggles folder open property for the given folder. + * + * @param {Object} folder + * @return {Array} + */ + toggleFolder(folder) { + return this.updateFolder(folder, 'isOpen', !folder.isOpen); + } + + /** + * Updates the folder with the received environments. + * + * + * @param {Object} folder Folder to update + * @param {Array} environments Received environments + * @return {Object} + */ + setfolderContent(folder, environments) { + const updatedEnvironments = environments.map((env) => { + let updated = env; + + if (env.latest) { + updated = Object.assign({}, env, env.latest); + delete updated.latest; + } else { + updated = env; + } + + updated.isChildren = true; + + return updated; + }); + + return this.updateFolder(folder, 'children', updatedEnvironments); + } + + /** + * Given a folder a prop and a new value updates the correct folder. + * + * @param {Object} folder + * @param {String} prop + * @param {String|Boolean|Object|Array} newValue + * @return {Array} + */ + updateFolder(folder, prop, newValue) { + const environments = this.state.environments; + + const updatedEnvironments = environments.map((env) => { + const updateEnv = Object.assign({}, env); + if (env.isFolder && env.id === folder.id) { + updateEnv[prop] = newValue; + } + + return updateEnv; + }); + + this.state.environments = updatedEnvironments; + + return updatedEnvironments; + } + } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index c6bb7fda8f2..22352950452 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -384,7 +384,7 @@ import FilteredSearchContainer from './container'; paths.push(`search=${sanitized}`); } - const parameterizedUrl = `?scope=all&utf8=โ&${paths.join('&')}`; + const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`; if (this.updateObject) { this.updateObject(parameterizedUrl); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js index 9bf1b1ced88..a2729dc0e95 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js @@ -8,21 +8,31 @@ require('./filtered_search_token_keys'); // Values that start with a double quote must end in a double quote (same for single) const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); const tokens = []; + const tokenIndexes = []; // stores key+value for simple search let lastToken = null; const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { let tokenValue = v1 || v2 || v3; let tokenSymbol = symbol; + let tokenIndex = ''; if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { tokenSymbol = tokenValue; tokenValue = ''; } - tokens.push({ - key, - value: tokenValue || '', - symbol: tokenSymbol || '', - }); + tokenIndex = `${key}:${tokenValue}`; + + // Prevent adding duplicates + if (tokenIndexes.indexOf(tokenIndex) === -1) { + tokenIndexes.push(tokenIndex); + + tokens.push({ + key, + value: tokenValue || '', + symbol: tokenSymbol || '', + }); + } + return ''; }).replace(/\s{2,}/g, ' ').trim() || ''; diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js index 6a028f299b1..62675d7e67e 100644 --- a/app/assets/javascripts/group_name.js +++ b/app/assets/javascripts/group_name.js @@ -1,40 +1,64 @@ -const GROUP_LIMIT = 2; + +import _ from 'underscore'; export default class GroupName { constructor() { - this.titleContainer = document.querySelector('.title'); - this.groups = document.querySelectorAll('.group-path'); + 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(); } init() { - if (this.groups.length > GROUP_LIMIT) { + if (this.groups.length > 0) { this.groups[this.groups.length - 1].classList.remove('hidable'); - this.addToggle(); + this.toggleHandler(); + window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100)); } this.render(); } - addToggle() { - const header = document.querySelector('.header-content'); + toggleHandler() { + if (this.titleWidth > this.titleContainer.offsetWidth) { + if (!this.toggle) this.createToggle(); + this.showToggle(); + } else if (this.toggle) { + this.hideToggle(); + } + } + + createToggle() { this.toggle = document.createElement('button'); this.toggle.className = 'text-expander group-name-toggle'; this.toggle.setAttribute('aria-label', 'Toggle full path'); this.toggle.innerHTML = '...'; this.toggle.addEventListener('click', this.toggleGroups.bind(this)); - header.insertBefore(this.toggle, this.titleContainer); + this.titleContainer.insertBefore(this.toggle, this.title); this.toggleGroups(); } + showToggle() { + this.title.classList.add('wrap'); + this.toggle.classList.remove('hidden'); + if (this.isHidden) this.groupTitle.classList.add('is-hidden'); + } + + hideToggle() { + this.title.classList.remove('wrap'); + this.toggle.classList.add('hidden'); + if (this.isHidden) this.groupTitle.classList.remove('is-hidden'); + } + toggleGroups() { this.isHidden = !this.isHidden; this.groupTitle.classList.toggle('is-hidden'); } render() { - this.titleContainer.classList.remove('initializing'); + this.title.classList.remove('initializing'); } } diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index e5dfa30edab..10363c16bae 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -6,23 +6,60 @@ var slice = [].slice; window.GroupsSelect = (function() { function GroupsSelect() { $('.ajax-groups-select').each((function(_this) { + const self = _this; + return function(i, select) { var all_available, skip_groups; - all_available = $(select).data('all-available'); - skip_groups = $(select).data('skip-groups') || []; - return $(select).select2({ + const $select = $(select); + all_available = $select.data('all-available'); + skip_groups = $select.data('skip-groups') || []; + + $select.select2({ placeholder: "Search for a group", - multiple: $(select).hasClass('multiselect'), + multiple: $select.hasClass('multiselect'), minimumInputLength: 0, - query: function(query) { - var options = { all_available: all_available, skip_groups: skip_groups }; - return Api.groups(query.term, options, function(groups) { - var data; - data = { - results: groups + ajax: { + url: Api.buildUrl(Api.groupsPath), + dataType: 'json', + quietMillis: 250, + transport: function (params) { + $.ajax(params).then((data, status, xhr) => { + const results = data || []; + + const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders()); + const currentPage = parseInt(headers['X-PAGE'], 10) || 0; + const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; + const more = currentPage < totalPages; + + return { + results, + pagination: { + more, + }, + }; + }).then(params.success).fail(params.error); + }, + data: function (search, page) { + return { + search, + page, + per_page: GroupsSelect.PER_PAGE, + all_available, + }; + }, + results: function (data, page) { + if (data.length) return { results: [] }; + + const groups = data.length ? data : data.results || []; + const more = data.pagination ? data.pagination.more : false; + const results = groups.filter(group => skip_groups.indexOf(group.id) === -1); + + return { + results, + page, + more, }; - return query.callback(data); - }); + }, }, initSelection: function(element, callback) { var id; @@ -34,19 +71,23 @@ window.GroupsSelect = (function() { formatResult: function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatResult.apply(_this, args); + return self.formatResult.apply(self, args); }, formatSelection: function() { var args; args = 1 <= arguments.length ? slice.call(arguments, 0) : []; - return _this.formatSelection.apply(_this, args); + return self.formatSelection.apply(self, args); }, - dropdownCssClass: "ajax-groups-dropdown", + dropdownCssClass: "ajax-groups-dropdown select2-infinite", // we do not want to escape markup since we are displaying html in results escapeMarkup: function(m) { return m; } }); + + self.dropdown = document.querySelector('.select2-infinite .select2-results'); + + $select.on('select2-loaded', self.forceOverflow.bind(self)); }; })(this)); } @@ -65,5 +106,12 @@ window.GroupsSelect = (function() { return group.full_name; }; + GroupsSelect.prototype.forceOverflow = function (e) { + const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight; + this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`; + }; + + GroupsSelect.PER_PAGE = 20; + return GroupsSelect; })(); diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 34f44dad7a5..dc170c60456 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */ $(document).on('todo:toggle', function(e, count) { - var $todoPendingCount = $('.todos-pending-count'); + var $todoPendingCount = $('.todos-count'); $todoPendingCount.text(gl.text.highCountTrim(count)); $todoPendingCount.toggleClass('hidden', count === 0); }); diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js index 357b3487ca9..aec13e78f42 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js +++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js @@ -1,4 +1,4 @@ -/* global Vue */ +import Vue from 'vue'; import stopwatchSvg from 'icons/_icon_stopwatch.svg'; require('../../../lib/utils/pretty_time'); diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js index 750468c679b..c55e263f6f4 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js +++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js @@ -1,4 +1,5 @@ -/* global Vue */ +import Vue from 'vue'; + require('../../../lib/utils/pretty_time'); (() => { diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js index 309e9f2f9ef..a7fbd704c40 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js +++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js @@ -1,4 +1,5 @@ -/* global Vue */ +import Vue from 'vue'; + (() => { Vue.component('time-tracking-estimate-only-pane', { name: 'time-tracking-estimate-only-pane', diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js b/app/assets/javascripts/issuable/time_tracking/components/help_state.js index d7ced6d7151..344b29ebea4 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/help_state.js +++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js @@ -1,4 +1,5 @@ -/* global Vue */ +import Vue from 'vue'; + (() => { Vue.component('time-tracking-help-state', { name: 'time-tracking-help-state', diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js index 1d2ca643b5b..b081adf5e64 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js +++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js @@ -1,4 +1,5 @@ -/* global Vue */ +import Vue from 'vue'; + (() => { Vue.component('time-tracking-no-tracking-pane', { name: 'time-tracking-no-tracking-pane', diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js index ed283fec3c3..edb9169112f 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js +++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js @@ -1,4 +1,5 @@ -/* global Vue */ +import Vue from 'vue'; + (() => { Vue.component('time-tracking-spent-only-pane', { name: 'time-tracking-spent-only-pane', diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js index 1fae2d62b14..0213522f551 100644 --- a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js +++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js @@ -1,4 +1,4 @@ -/* global Vue */ +import Vue from 'vue'; require('./help_state'); require('./collapsed_state'); diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js index 0134b7cb6f3..1689a69e1ed 100644 --- a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js +++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js @@ -1,11 +1,12 @@ -/* global Vue */ +import Vue from 'vue'; +import VueResource from 'vue-resource'; -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); require('./components/time_tracker'); require('../../smart_interval'); require('../../subbable_resource'); +Vue.use(VueResource); + (() => { /* This Vue instance represents what will become the parent instance for the * sidebar. It will be responsible for managing `issuable` state and propagating diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 115312d4b83..834b98e8601 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,8 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, max-len */ /* global UsersSelect */ -/* global Cookies */ /* global bp */ +import Cookies from 'js-cookie'; + (function() { this.IssuableContext = (function() { function IssuableContext(currentUser) { diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js new file mode 100644 index 00000000000..b6ce8e83729 --- /dev/null +++ b/app/assets/javascripts/issue_show/index.js @@ -0,0 +1,26 @@ +import Vue from 'vue'; +import IssueTitle from './issue_title'; +import '../vue_shared/vue_resource_interceptor'; + +const vueOptions = () => ({ + el: '.issue-title-entrypoint', + components: { + IssueTitle, + }, + data() { + const issueTitleData = document.querySelector('.issue-title-data').dataset; + + return { + initialTitle: issueTitleData.initialTitle, + endpoint: issueTitleData.endpoint, + }; + }, + template: ` + <IssueTitle + :initialTitle="initialTitle" + :endpoint="endpoint" + /> + `, +}); + +(() => new Vue(vueOptions()))(); diff --git a/app/assets/javascripts/issue_show/issue_title.js b/app/assets/javascripts/issue_show/issue_title.js new file mode 100644 index 00000000000..1184c8956dc --- /dev/null +++ b/app/assets/javascripts/issue_show/issue_title.js @@ -0,0 +1,78 @@ +import Visibility from 'visibilityjs'; +import Poll from './../lib/utils/poll'; +import Service from './services/index'; + +export default { + props: { + initialTitle: { required: true, type: String }, + endpoint: { required: true, type: String }, + }, + data() { + const resource = new Service(this.$http, this.endpoint); + + const poll = new Poll({ + resource, + method: 'getTitle', + successCallback: (res) => { + this.renderResponse(res); + }, + errorCallback: (err) => { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.error('ISSUE SHOW TITLE REALTIME ERROR', err); + } else { + throw new Error(err); + } + }, + }); + + return { + poll, + timeoutId: null, + title: this.initialTitle, + }; + }, + methods: { + fetch() { + this.poll.makeRequest(); + + Visibility.change(() => { + if (!Visibility.hidden()) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + }, + renderResponse(res) { + const body = JSON.parse(res.body); + this.triggerAnimation(body); + }, + triggerAnimation(body) { + const { title } = body; + + /** + * since opacity is changed, even if there is no diff for Vue to update + * we must check the title even on a 304 to ensure no visual change + */ + if (this.title === title) return; + + this.$el.style.opacity = 0; + + this.timeoutId = setTimeout(() => { + this.title = title; + + this.$el.style.transition = 'opacity 0.2s ease'; + this.$el.style.opacity = 1; + + clearTimeout(this.timeoutId); + }, 100); + }, + }, + created() { + this.fetch(); + }, + template: ` + <h2 class='title' v-html='title'></h2> + `, +}; diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js new file mode 100644 index 00000000000..c4ab0b1e07a --- /dev/null +++ b/app/assets/javascripts/issue_show/services/index.js @@ -0,0 +1,10 @@ +export default class Service { + constructor(resource, endpoint) { + this.resource = resource; + this.endpoint = endpoint; + } + + getTitle() { + return this.resource.get(this.endpoint); + } +} diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 08ca9e4fa4d..a5f99bcdd8f 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -11,8 +11,9 @@ }); }; - $(function() { - var $scrollingTabs = $('.scrolling-tabs'); + $(document).on('init.scrolling-tabs', () => { + const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); + $scrollingTabs.addClass('is-initialized'); hideEndFade($scrollingTabs); $(window).off('resize.nav').on('resize.nav', function() { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index a1423b6fda5..46b80c04e20 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -232,6 +232,22 @@ }; /** + this will take in the getAllResponseHeaders result and normalize them + this way we don't run into production issues when nginx gives us lowercased header keys + */ + w.gl.utils.normalizeCRLFHeaders = (headers) => { + const headersObject = {}; + const headersArray = headers.split('\n'); + + headersArray.forEach((header) => { + const keyValue = header.split(': '); + headersObject[keyValue[0]] = keyValue[1]; + }); + + return w.gl.utils.normalizeHeaders(headersObject); + }; + + /** * Parses pagination object string values into numbers. * * @param {Object} paginationInformation @@ -247,7 +263,7 @@ }); /** - * Updates the search parameter of a URL given the parameter and values provided. + * Updates the search parameter of a URL given the parameter and value provided. * * If no search params are present we'll add it. * If param for page is already present, we'll update it @@ -262,17 +278,24 @@ let search; const locationSearch = window.location.search; - if (locationSearch.length === 0) { - search = `?${param}=${value}`; - } + if (locationSearch.length) { + const parameters = locationSearch.substring(1, locationSearch.length) + .split('&') + .reduce((acc, element) => { + const val = element.split('='); + acc[val[0]] = decodeURIComponent(val[1]); + return acc; + }, {}); - if (locationSearch.indexOf(param) !== -1) { - const regex = new RegExp(param + '=\\d'); - search = locationSearch.replace(regex, `${param}=${value}`); - } + parameters[param] = value; - if (locationSearch.length && locationSearch.indexOf(param) === -1) { - search = `${locationSearch}&${param}=${value}`; + const toString = Object.keys(parameters) + .map(val => `${val}=${encodeURIComponent(parameters[val])}`) + .join('&'); + + search = `?${toString}`; + } else { + search = `?${param}=${value}`; } return search; diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js new file mode 100644 index 00000000000..e2bf69ee52e --- /dev/null +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -0,0 +1,34 @@ +/* eslint-disable import/prefer-default-export */ + +/** + * Function that allows a number with an X amount of decimals + * to be formatted in the following fashion: + * * For 1 digit to the left of the decimal point and X digits to the right of it + * * * Show 3 digits to the right + * * For 2 digits to the left of the decimal point and X digits to the right of it + * * * Show 2 digits to the right +*/ +export function formatRelevantDigits(number) { + let digitsLeft = ''; + let relevantDigits = 0; + let formattedNumber = ''; + if (!isNaN(Number(number))) { + digitsLeft = number.split('.')[0]; + switch (digitsLeft.length) { + case 1: + relevantDigits = 3; + break; + case 2: + relevantDigits = 2; + break; + case 3: + relevantDigits = 1; + break; + default: + relevantDigits = 4; + break; + } + formattedNumber = Number(number).toFixed(relevantDigits); + } + return formattedNumber; +} diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js new file mode 100644 index 00000000000..5c22aea51cd --- /dev/null +++ b/app/assets/javascripts/lib/utils/poll.js @@ -0,0 +1,100 @@ +import httpStatusCodes from './http_status'; + +/** + * Polling utility for handling realtime updates. + * Service for vue resouce and method need to be provided as props + * + * @example + * new Poll({ + * resource: resource, + * method: 'name', + * data: {page: 1, scope: 'all'}, // optional + * successCallback: () => {}, + * errorCallback: () => {}, + * notificationCallback: () => {}, // optional + * }).makeRequest(); + * + * Usage in pipelines table with visibility lib: + * + * const poll = new Poll({ + * resource: this.service, + * method: 'getPipelines', + * data: { page: pageNumber, scope }, + * successCallback: this.successCallback, + * errorCallback: this.errorCallback, + * notificationCallback: this.updateLoading, + * }); + * + * if (!Visibility.hidden()) { + * poll.makeRequest(); + * } + * + * Visibility.change(() => { + * if (!Visibility.hidden()) { + * poll.restart(); + * } else { + * poll.stop(); + * } +* }); + * + * 1. Checks for response and headers before start polling + * 2. Interval is provided by `Poll-Interval` header. + * 3. If `Poll-Interval` is -1, we stop polling + * 4. If HTTP response is 200, we poll. + * 5. If HTTP response is different from 200, we stop polling. + * + */ +export default class Poll { + constructor(options = {}) { + this.options = options; + this.options.data = options.data || {}; + this.options.notificationCallback = options.notificationCallback || + function notificationCallback() {}; + + this.intervalHeader = 'POLL-INTERVAL'; + this.timeoutID = null; + this.canPoll = true; + } + + checkConditions(response) { + const headers = gl.utils.normalizeHeaders(response.headers); + const pollInterval = parseInt(headers[this.intervalHeader], 10); + + if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) { + this.timeoutID = setTimeout(() => { + this.makeRequest(); + }, pollInterval); + } + + this.options.successCallback(response); + } + + makeRequest() { + const { resource, method, data, errorCallback, notificationCallback } = this.options; + + // It's called everytime a new request is made. Useful to update the status. + notificationCallback(true); + + return resource[method](data) + .then(response => this.checkConditions(response)) + .catch(error => errorCallback(error)); + } + + /** + * Stops the polling recursive chain + * and guarantees if the timeout is already running it won't make another request by + * cancelling the previously established timeout. + */ + stop() { + this.canPoll = false; + clearTimeout(this.timeoutID); + } + + /** + * Restarts polling after it has been stoped + */ + restart() { + this.canPoll = true; + this.makeRequest(); + } +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 81d5748191d..5b50bc62876 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ /* global bp */ -/* global Cookies */ /* global Flash */ /* global ConfirmDangerModal */ /* global Aside */ @@ -24,7 +23,6 @@ import './extensions/array'; window.jQuery = jQuery; window.$ = jQuery; window._ = _; -window.Cookies = Cookies; window.Pikaday = Pikaday; window.Dropzone = Dropzone; window.Sortable = Sortable; @@ -49,15 +47,6 @@ import { installGlEmojiElement } from './behaviors/gl_emoji'; installGlEmojiElement(); // blob -import './blob/blob_ci_yaml'; -import './blob/blob_dockerfile_selector'; -import './blob/blob_dockerfile_selectors'; -import './blob/blob_file_dropzone'; -import './blob/blob_gitignore_selector'; -import './blob/blob_gitignore_selectors'; -import './blob/blob_license_selector'; -import './blob/blob_license_selectors'; -import './blob/template_selector'; import './blob/create_branch_dropdown'; import './blob/target_branch_dropdown'; @@ -198,6 +187,9 @@ import './visibility_select'; import './wikis'; import './zen_mode'; +// eslint-disable-next-line global-require +if (process.env.NODE_ENV !== 'production') require('./test_utils/'); + document.addEventListener('beforeunload', function () { // Unbind scroll events $(document).off('scroll'); @@ -381,4 +373,6 @@ $(function () { new Aside(); gl.utils.initTimeagoTimeout(); + + $(document).trigger('init.scrolling-tabs'); }); diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index c7e78fed8fe..645045fea88 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js @@ -1,8 +1,9 @@ /* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, no-new, no-param-reassign, max-len */ -/* global Vue */ /* global ace */ /* global Flash */ +import Vue from 'vue'; + ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js index 240c8f98932..56d6678e1bd 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign, comma-dangle */ -/* global Vue */ + +import Vue from 'vue'; ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js index 97753c50b60..0fc4a13450a 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign, comma-dangle */ -/* global Vue */ + +import Vue from 'vue'; ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js index 74587df22c5..c4e379a4a0b 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js @@ -1,6 +1,7 @@ /* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */ -/* global Cookies */ -/* global Vue */ + +import Vue from 'vue'; +import Cookies from 'js-cookie'; ((global) => { global.mergeConflicts = global.mergeConflicts || {}; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 653e52fb6bf..15992460146 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -1,8 +1,8 @@ /* eslint-disable new-cap, comma-dangle, no-new */ -/* global Vue */ /* global Flash */ -window.Vue = require('vue'); +import Vue from 'vue'; + require('./merge_conflict_store'); require('./merge_conflict_service'); require('./mixins/line_conflict_utils'); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 190336dbd20..3c4e6102469 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,11 +1,13 @@ /* eslint-disable no-new, class-methods-use-this */ /* global Breakpoints */ -/* global Cookies */ /* global Flash */ -require('./breakpoints'); -window.Cookies = require('js-cookie'); -require('./flash'); +import Cookies from 'js-cookie'; + +import CommitPipelinesTable from './commit/pipelines/pipelines_table'; + +import './breakpoints'; +import './flash'; /* eslint-disable max-len */ // MergeRequestTabs @@ -97,6 +99,13 @@ require('./flash'); .off('click', this.clickTab); } + destroy() { + this.unbindEvents(); + if (this.commitPipelinesTable) { + this.commitPipelinesTable.$destroy(); + } + } + showTab(e) { e.preventDefault(); this.activateTab($(e.target).data('action')); @@ -127,16 +136,9 @@ require('./flash'); if (this.diffViewType() === 'parallel') { this.expandViewContainer(); } - $.scrollTo('.merge-request-details .merge-request-tabs', { - offset: 0, - }); } else if (action === 'pipelines') { - if (this.pipelinesLoaded) { - return; - } - const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); - this.pipelinesLoaded = true; + this.resetViewContainer(); + this.loadPipelines(); } else { this.expandView(); this.resetViewContainer(); @@ -225,6 +227,18 @@ require('./flash'); }); } + loadPipelines() { + if (this.pipelinesLoaded) { + return; + } + const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); + // Could already be mounted from the `pipelines_bundle` + if (pipelineTableViewEl) { + this.commitPipelinesTable = new CommitPipelinesTable().$mount(pipelineTableViewEl); + } + this.pipelinesLoaded = true; + } + loadDiff(source) { if (this.diffsLoaded) { return; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 40e977df693..ac4fad88fe5 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,8 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */ -/* global Vue */ /* global Issuable */ /* global ListMilestone */ +import Vue from 'vue'; + (function() { this.MilestoneSelect = (function() { function MilestoneSelect(currentProject, els) { diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js index fcffc11a2df..a6ffa0f59de 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -3,7 +3,7 @@ import d3 from 'd3'; import statusCodes from '~/lib/utils/http_status'; -import '../lib/utils/common_utils'; +import { formatRelevantDigits } from '~/lib/utils/number_utils'; import '../flash'; const prometheusGraphsContainer = '.prometheus-graph'; @@ -21,19 +21,19 @@ class PrometheusGraph { const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + extraAddedWidthParent; this.originalWidth = parentContainerWidth; - this.originalHeight = 400; + this.originalHeight = 330; this.width = parentContainerWidth - this.margin.left - this.margin.right; - this.height = 400 - this.margin.top - this.margin.bottom; + this.height = this.originalHeight - this.margin.top - this.margin.bottom; this.backOffRequestCounter = 0; this.configureGraph(); this.init(); } createGraph() { - Object.keys(this.data).forEach((key) => { - const value = this.data[key]; - if (value.length > 0) { - this.plotValues(value, key); + Object.keys(this.graphSpecificProperties).forEach((key) => { + const value = this.graphSpecificProperties[key]; + if (value.data.length > 0) { + this.plotValues(key); } }); } @@ -49,53 +49,56 @@ class PrometheusGraph { }); } - plotValues(valuesToPlot, key) { + 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]); - const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; + graphSpecifics.xScale = x; + graphSpecifics.yScale = y; - const graphSpecifics = this.graphSpecificProperties[key]; + 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('transform', `translate(${this.margin.left},${this.margin.top})`); + .attr('width', this.width + this.margin.left + this.margin.right) + .attr('height', this.height + this.margin.bottom + this.margin.top) + .append('g') + .attr('transform', `translate(${this.margin.left},${this.margin.top})`); const axisLabelContainer = d3.select(prometheusGraphContainer) - .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right) - .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top) + .attr('width', this.originalWidth) + .attr('height', this.originalHeight) .append('g') .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`); - x.domain(d3.extent(valuesToPlot, d => d.time)); - y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]); + 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'); + .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) - .orient('left'); + .scale(y) + .ticks(this.commonGraphProperties.axis_no_ticks) + .tickSize(-this.width) + .orient('left'); this.createAxisLabelContainers(axisLabelContainer, key); chart.append('g') - .attr('class', 'x-axis') - .attr('transform', `translate(0,${this.height})`) - .call(xAxis); + .attr('class', 'x-axis') + .attr('transform', `translate(0,${this.height})`) + .call(xAxis); chart.append('g') - .attr('class', 'y-axis') - .call(yAxis); + .attr('class', 'y-axis') + .call(yAxis); const area = d3.svg.area() .x(d => x(d.time)) @@ -108,13 +111,13 @@ class PrometheusGraph { .y(d => y(d.value)); chart.append('path') - .datum(valuesToPlot) - .attr('d', area) - .attr('class', 'metric-area') - .attr('fill', graphSpecifics.area_fill_color); + .datum(graphSpecifics.data) + .attr('d', area) + .attr('class', 'metric-area') + .attr('fill', graphSpecifics.area_fill_color); chart.append('path') - .datum(valuesToPlot) + .datum(graphSpecifics.data) .attr('class', 'metric-line') .attr('stroke', graphSpecifics.line_color) .attr('fill', 'none') @@ -126,7 +129,7 @@ class PrometheusGraph { .attr('class', 'prometheus-graph-overlay') .attr('width', this.width) .attr('height', this.height) - .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key)); + .on('mousemove', this.handleMouseOverGraph.bind(this, prometheusGraphContainer)); } // The legends from the metric @@ -134,128 +137,150 @@ class PrometheusGraph { const graphSpecifics = this.graphSpecificProperties[key]; axisLabelContainer.append('line') - .attr('class', 'label-x-axis-line') - .attr('stroke', '#000000') - .attr('stroke-width', '1') - .attr({ - x1: 0, - y1: this.originalHeight - this.marginLabelContainer.top, - x2: this.originalWidth - this.margin.right, - y2: this.originalHeight - this.marginLabelContainer.top, - }); + .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: 0, - y1: 0, - x2: 0, - y2: this.originalHeight - this.marginLabelContainer.top, - }); + .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.marginLabelContainer.top) / 2}) rotate(-90)`) - .text(graphSpecifics.graph_legend_title); + .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 - this.marginLabelContainer.top - 20) - .attr('width', 30) - .attr('height', 80); + .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.marginLabelContainer.top) - .attr('dy', '.35em') - .text('Time'); + .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) - 80) - .style('fill', graphSpecifics.area_fill_color) - .attr('width', 20) - .attr('height', 35); + .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', 'label-axis-text') - .attr('x', this.originalWidth - 140) - .attr('y', (this.originalHeight / 2) - 65) - .text(graphSpecifics.graph_legend_title); + .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) - 50); + .attr('class', 'text-metric-usage') + .attr('x', this.originalWidth - 140) + .attr('y', (this.originalHeight / 2) - 25); } - handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) { + handleMouseOverGraph(prometheusGraphContainer) { const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`); - const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]); - const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1); - const d0 = valuesToPlot[timeValueIndex - 1]; - const d1 = valuesToPlot[timeValueIndex]; - const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0; - const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value))); - const currentTimeCoordinate = x(currentData.time); - const graphSpecifics = this.graphSpecificProperties[key]; - // Remove the current selectors - d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove(); - d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove(); - d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove(); - d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove(); - - chart.append('line') - .attr('class', 'selected-metric-line') - .attr({ - x1: currentTimeCoordinate, - y1: y(0), - x2: currentTimeCoordinate, - y2: maxValueMetric, - }); + 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 = currentGraphProps.xScale(currentData.time); + 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`).remove(); + d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove(); + + const currentChart = d3.select(currentPrometheusGraphContainer).select('g'); + currentChart.append('line') + .attr('class', 'selected-metric-line') + .attr({ + x1: currentTimeCoordinate, + y1: currentGraphProps.yScale(0), + x2: currentTimeCoordinate, + y2: maxMetricValue, + }); + + currentChart.append('circle') + .attr('class', 'circle-metric') + .attr('fill', currentGraphProps.line_color) + .attr('cx', currentTimeCoordinate) + .attr('cy', currentGraphProps.yScale(currentData.value)) + .attr('r', this.commonGraphProperties.circle_radius_metric); + + // The little box with text + const rectTextMetric = currentChart.append('g') + .attr('class', 'rect-text-metric') + .attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`); + + rectTextMetric.append('rect') + .attr('class', 'rect-metric') + .attr('x', currentTimeCoordinate + 10) + .attr('y', maxMetricValue) + .attr('width', this.commonGraphProperties.rect_text_width) + .attr('height', this.commonGraphProperties.rect_text_height); + + rectTextMetric.append('text') + .attr('class', 'text-metric') + .attr('x', currentTimeCoordinate + 35) + .attr('y', maxMetricValue + 35) + .text(timeFormat(currentData.time)); + + rectTextMetric.append('text') + .attr('class', 'text-metric-date') + .attr('x', currentTimeCoordinate + 15) + .attr('y', maxMetricValue + 15) + .text(dayFormat(currentData.time)); + + let currentMetricValue = formatRelevantDigits(currentData.value); + if (key === 'cpu_values') { + currentMetricValue = `${currentMetricValue}%`; + } else { + currentMetricValue = `${currentMetricValue} MB`; + } - chart.append('circle') - .attr('class', 'circle-metric') - .attr('fill', graphSpecifics.line_color) - .attr('cx', currentTimeCoordinate) - .attr('cy', y(currentData.value)) - .attr('r', this.commonGraphProperties.circle_radius_metric); - - // The little box with text - const rectTextMetric = chart.append('g') - .attr('class', 'rect-text-metric') - .attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`); - - rectTextMetric.append('rect') - .attr('class', 'rect-metric') - .attr('x', currentTimeCoordinate + 10) - .attr('y', maxValueMetric) - .attr('width', this.commonGraphProperties.rect_text_width) - .attr('height', this.commonGraphProperties.rect_text_height); - - rectTextMetric.append('text') - .attr('class', 'text-metric') - .attr('x', currentTimeCoordinate + 35) - .attr('y', maxValueMetric + 35) - .text(timeFormat(currentData.time)); - - rectTextMetric.append('text') - .attr('class', 'text-metric-date') - .attr('x', currentTimeCoordinate + 15) - .attr('y', maxValueMetric + 15) - .text(dayFormat(currentData.time)); - - // Update the text - d3.select(`${prometheusGraphContainer} .text-metric-usage`) - .text(currentData.value.substring(0, 8)); + d3.select(`${currentPrometheusGraphContainer} .text-metric-usage`) + .text(currentMetricValue); + }); } configureGraph() { @@ -264,11 +289,17 @@ class PrometheusGraph { 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: {}, }, }; @@ -318,17 +349,17 @@ class PrometheusGraph { } transformData(metricsResponse) { - const metricTypes = {}; Object.keys(metricsResponse.metrics).forEach((key) => { if (key === 'cpu_values' || key === 'memory_values') { const metricValues = (metricsResponse.metrics[key])[0]; - metricTypes[key] = metricValues.values.map(metric => ({ - time: new Date(metric[0] * 1000), - value: metric[1], - })); + if (typeof metricValues !== 'undefined') { + this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ + time: new Date(metric[0] * 1000), + value: metric[1], + })); + } } }); - this.data = metricTypes; } } diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 47cc34e7a20..1d563c63f39 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,14 +1,14 @@ /* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, 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 */ /* global Flash */ /* global Autosave */ -/* global Cookies */ /* global ResolveService */ /* global mrRefreshWidgetUrl */ +import Cookies from 'js-cookie'; + require('./autosave'); window.autosize = require('vendor/autosize'); window.Dropzone = require('dropzone'); -window.Cookies = require('js-cookie'); require('./dropzone_input'); require('./gfm_auto_complete'); require('vendor/jquery.caret'); // required by jquery.atwho diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js index c38bc762675..4ccea0624ee 100644 --- a/app/assets/javascripts/profile/profile.js +++ b/app/assets/javascripts/profile/profile.js @@ -25,6 +25,7 @@ bindEvents() { $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm); $('#user_notification_email').on('change', this.submitForm); + $('#user_notified_of_own_activity').on('change', this.submitForm); $('.update-username').on('ajax:before', this.beforeUpdateUsername); $('.update-username').on('ajax:complete', this.afterUpdateUsername); $('.update-notifications').on('ajax:success', this.onUpdateNotifs); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index db7ceaa2421..f944fcc5a58 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,7 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ -/* global Cookies */ /* global ProjectSelect */ +import Cookies from 'js-cookie'; + (function() { this.Project = (function() { function Project() { diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js index 5cf28aa7a73..1d4bb8a13d6 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js @@ -6,7 +6,7 @@ class ProtectedBranchDropdown { this.$dropdown = options.$dropdown; this.$dropdownContainer = this.$dropdown.parent(); this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); - this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch'); + this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch'); this.buildDropdown(); this.bindEvents(); @@ -46,7 +46,9 @@ class ProtectedBranchDropdown { this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); } - onClickCreateWildcard() { + onClickCreateWildcard(e) { + e.preventDefault(); + // Refresh the dropdown's data, which ends up calling `getProtectedBranches` this.$dropdown.data('glDropdown').remote.execute(); this.$dropdown.data('glDropdown').selectRowAtIndex(); @@ -69,7 +71,7 @@ class ProtectedBranchDropdown { if (branchName) { this.$dropdownContainer - .find('.create-new-protected-branch code') + .find('.js-create-new-protected-branch code') .text(branchName); } diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index 48cae8a4fa9..ea91aaa10a6 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len */ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */ + // Render Gitlab flavoured Markdown // // Delegates to syntax highlight and render math diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js index 76c61c001ba..8b3fee49cb9 100644 --- a/app/assets/javascripts/render_math.js +++ b/app/assets/javascripts/render_math.js @@ -1,4 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len, no-console */ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len, no-console */ +/* global katex */ + // Renders math using KaTeX in any element with the // `js-render-math` class // diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index 903862cac6b..a9b3de281e1 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,5 +1,6 @@ /* 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 */ -/* global Cookies */ + +import Cookies from 'js-cookie'; (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -55,14 +56,15 @@ Sidebar.prototype.toggleTodo = function(e) { var $btnText, $this, $todoLoading, ajaxType, url; $this = $(e.currentTarget); - $todoLoading = $('.js-issuable-todo-loading'); - $btnText = $('.js-issuable-todo-text', $this); ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST'; if ($this.attr('data-delete-path')) { url = "" + ($this.attr('data-delete-path')); } else { url = "" + ($this.data('url')); } + + $this.tooltip('hide'); + return $.ajax({ url: url, type: ajaxType, @@ -73,34 +75,44 @@ }, beforeSend: (function(_this) { return function() { - return _this.beforeTodoSend($this, $todoLoading); + $('.js-issuable-todo').disable() + .addClass('is-loading'); }; })(this) }).done((function(_this) { return function(data) { - return _this.todoUpdateDone(data, $this, $btnText, $todoLoading); + return _this.todoUpdateDone(data); }; })(this)); }; - Sidebar.prototype.beforeTodoSend = function($btn, $todoLoading) { - $btn.disable(); - return $todoLoading.removeClass('hidden'); - }; + Sidebar.prototype.todoUpdateDone = function(data) { + const deletePath = data.delete_path ? data.delete_path : null; + const attrPrefix = deletePath ? 'mark' : 'todo'; + const $todoBtns = $('.js-issuable-todo'); - Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) { $(document).trigger('todo:toggle', data.count); - $btn.enable(); - $todoLoading.addClass('hidden'); + $todoBtns.each((i, el) => { + const $el = $(el); + const $elText = $el.find('.js-issuable-todo-inner'); - if (data.delete_path != null) { - $btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path); - return $btnText.text($btn.data('mark-text')); - } else { - $btn.attr('aria-label', $btn.data('todo-text')).removeAttr('data-delete-path'); - return $btnText.text($btn.data('todo-text')); - } + $el.removeClass('is-loading') + .enable() + .attr('aria-label', $el.data(`${attrPrefix}-text`)) + .attr('data-delete-path', deletePath) + .attr('title', $el.data(`${attrPrefix}-text`)); + + if ($el.hasClass('has-tooltip')) { + $el.tooltip('fixTitle'); + } + + if ($el.data(`${attrPrefix}-icon`)) { + $elText.html($el.data(`${attrPrefix}-icon`)); + } else { + $elText.text($el.data(`${attrPrefix}-text`)); + } + }); }; Sidebar.prototype.sidebarDropdownLoading = function(e) { @@ -197,9 +209,9 @@ }; Sidebar.prototype.setSidebarHeight = function() { - const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight(); + const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight(); const $rightSidebar = $('.js-right-sidebar'); - const diff = $navHeight - $('body').scrollTop(); + const diff = $navHeight - $(window).scrollTop(); if (diff > 0) { $rightSidebar.outerHeight($(window).height() - diff); } else { diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index 81766f4bd55..fd5097696ad 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -33,6 +33,10 @@ }; Shortcuts.prototype.toggleMarkdownPreview = function(e) { + // Check if short-cut was triggered while in Write Mode + if ($(e.target).hasClass('js-note-text')) { + $('.js-md-preview-button').focus(); + } return $(document).triggerHandler('markdown-preview:toggle', [e]); }; diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index e7baea894f6..4f1a19924a4 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -22,6 +22,9 @@ require('./shortcuts'); Mousetrap.bind('g m', function() { return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests'); }); + Mousetrap.bind('g t', function() { + return ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-todos'); + }); Mousetrap.bind('g p', function() { return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects'); }); diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 09a58cad2b2..3f5d6724417 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -43,6 +43,9 @@ require('./shortcuts'); Mousetrap.bind('g m', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests'); }); + Mousetrap.bind('g t', function() { + return ShortcutsNavigation.findAndFollowLink('.shortcuts-todos'); + }); Mousetrap.bind('g w', function() { return ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki'); }); diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js index 62d1604fe9e..9c307915ec4 100644 --- a/app/assets/javascripts/subscription.js +++ b/app/assets/javascripts/subscription.js @@ -1,4 +1,4 @@ -/* global Vue */ +import Vue from 'vue'; (() => { class Subscription { diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index e9e9aafd71a..e62f429f1ae 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -1,15 +1,15 @@ /* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ /* global Api */ -require('../blob/template_selector'); +import TemplateSelector from '../blob/template_selector'; ((global) => { - class IssuableTemplateSelector extends gl.TemplateSelector { + class IssuableTemplateSelector extends TemplateSelector { constructor(...args) { super(...args); this.projectPath = this.dropdown.data('project-path'); this.namespacePath = this.dropdown.data('namespace-path'); - this.issuableType = this.wrapper.data('issuable-type'); + this.issuableType = this.$dropdownContainer.data('issuable-type'); this.titleInput = $(`#${this.issuableType}_title`); const initialQuery = { @@ -41,16 +41,16 @@ require('../blob/template_selector'); } setInputValueToTemplateContent() { - // `this.requestFileSuccess` sets the value of the description input field + // `this.setEditorContent` sets the value of the description input field // to the content of the template selected. if (this.titleInput.val() === '') { // If the title has not yet been set, focus the title input and // skip focusing the description input by setting `true` as the - // `skipFocus` option to `requestFileSuccess`. - this.requestFileSuccess(this.currentTemplate, { skipFocus: true }); + // `skipFocus` option to `setEditorContent`. + this.setEditorContent(this.currentTemplate, { skipFocus: true }); this.titleInput.focus(); } else { - this.requestFileSuccess(this.currentTemplate, { skipFocus: false }); + this.setEditorContent(this.currentTemplate, { skipFocus: false }); } return; } diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js new file mode 100644 index 00000000000..ef401abce2d --- /dev/null +++ b/app/assets/javascripts/test_utils/index.js @@ -0,0 +1,4 @@ +import simulateDrag from './simulate_drag'; + +// Export to global space for rspec to use +window.simulateDrag = simulateDrag; diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js index d48f2404fa5..e39213cb098 100644 --- a/app/assets/javascripts/test_utils/simulate_drag.js +++ b/app/assets/javascripts/test_utils/simulate_drag.js @@ -1,143 +1,137 @@ -/* eslint-disable wrap-iife, func-names, strict, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, no-unused-expressions, prefer-arrow-callback, max-len */ -(function () { - 'use strict'; - - function simulateEvent(el, type, options) { - var event; - if (!el) return; - var ownerDocument = el.ownerDocument; - - options = options || {}; - - if (/^mouse/.test(type)) { - event = ownerDocument.createEvent('MouseEvents'); - event.initMouseEvent(type, true, true, ownerDocument.defaultView, - options.button, options.screenX, options.screenY, options.clientX, options.clientY, - options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); - } else { - event = ownerDocument.createEvent('CustomEvent'); - - event.initCustomEvent(type, true, true, ownerDocument.defaultView, - options.button, options.screenX, options.screenY, options.clientX, options.clientY, - options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); - - event.dataTransfer = { - data: {}, - - setData: function (type, val) { - this.data[type] = val; - }, - - getData: function (type) { - return this.data[type]; - } - }; - } - - if (el.dispatchEvent) { - el.dispatchEvent(event); - } else if (el.fireEvent) { - el.fireEvent('on' + type, event); - } - - return event; - } - - function isLast(target) { - var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; - var children = el.children; - - return children.length - 1 === target.index; +function simulateEvent(el, type, options = {}) { + let event; + if (!el) return null; + + if (/^mouse/.test(type)) { + event = el.ownerDocument.createEvent('MouseEvents'); + event.initMouseEvent(type, true, true, el.ownerDocument.defaultView, + options.button, options.screenX, options.screenY, options.clientX, options.clientY, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); + } else { + event = el.ownerDocument.createEvent('CustomEvent'); + + event.initCustomEvent(type, true, true, el.ownerDocument.defaultView, + options.button, options.screenX, options.screenY, options.clientX, options.clientY, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); + + event.dataTransfer = { + data: {}, + + setData(key, val) { + this.data[key] = val; + }, + + getData(key) { + return this.data[key]; + }, + }; } - function getTarget(target) { - var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; - var children = el.children; - - return ( - children[target.index] || - children[target.index === 'first' ? 0 : -1] || - children[target.index === 'last' ? children.length - 1 : -1] || - el - ); + if (el.dispatchEvent) { + el.dispatchEvent(event); + } else if (el.fireEvent) { + el.fireEvent(`on${type}`, event); } - function getRect(el) { - var rect = el.getBoundingClientRect(); - var width = rect.right - rect.left; - var height = rect.bottom - rect.top + 10; - - return { - x: rect.left, - y: rect.top, - cx: rect.left + width / 2, - cy: rect.top + height / 2, - w: width, - h: height, - hw: width / 2, - wh: height / 2 - }; + return event; +} + +function isLast(target) { + const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; + const children = el.children; + + return children.length - 1 === target.index; +} + +function getTarget(target) { + const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; + const children = el.children; + + return ( + children[target.index] || + children[target.index === 'first' ? 0 : -1] || + children[target.index === 'last' ? children.length - 1 : -1] || + el + ); +} + +function getRect(el) { + const rect = el.getBoundingClientRect(); + const width = rect.right - rect.left; + const height = (rect.bottom - rect.top) + 10; + + return { + x: rect.left, + y: rect.top, + cx: rect.left + (width / 2), + cy: rect.top + (height / 2), + w: width, + h: height, + hw: width / 2, + wh: height / 2, + }; +} + +export default function simulateDrag(options) { + const { to, from } = options; + to.el = to.el || from.el; + + const fromEl = getTarget(from); + const toEl = getTarget(to); + const firstEl = getTarget({ + el: to.el, + index: 'first', + }); + const lastEl = getTarget({ + el: options.to.el, + index: 'last', + }); + + const fromRect = getRect(fromEl); + const toRect = getRect(toEl); + const firstRect = getRect(firstEl); + const lastRect = getRect(lastEl); + + const startTime = new Date().getTime(); + const duration = options.duration || 1000; + + simulateEvent(fromEl, 'mousedown', { + button: 0, + clientX: fromRect.cx, + clientY: fromRect.cy, + }); + + if (options.ontap) options.ontap(); + window.SIMULATE_DRAG_ACTIVE = 1; + + if (options.to.index === 0) { + toRect.cy = firstRect.y; + } else if (isLast(options.to)) { + toRect.cy = lastRect.y + lastRect.h + 50; } - function simulateDrag(options, callback) { - options.to.el = options.to.el || options.from.el; + const dragInterval = setInterval(() => { + const progress = (new Date().getTime() - startTime) / duration; + const x = (fromRect.cx + ((toRect.cx - fromRect.cx) * progress)); + const y = (fromRect.cy + ((toRect.cy - fromRect.cy) * progress)); + const overEl = fromEl.ownerDocument.elementFromPoint(x, y); - var fromEl = getTarget(options.from); - var toEl = getTarget(options.to); - var firstEl = getTarget({ - el: options.to.el, - index: 'first' + simulateEvent(overEl, 'mousemove', { + clientX: x, + clientY: y, }); - var lastEl = getTarget({ - el: options.to.el, - index: 'last' - }); - var scrollable = options.scrollable; - - var fromRect = getRect(fromEl); - var toRect = getRect(toEl); - var firstRect = getRect(firstEl); - var lastRect = getRect(lastEl); - - var startTime = new Date().getTime(); - var duration = options.duration || 1000; - simulateEvent(fromEl, 'mousedown', { button: 0 }); - options.ontap && options.ontap(); - window.SIMULATE_DRAG_ACTIVE = 1; - - if (options.to.index === 0) { - toRect.cy = firstRect.y; - } else if (isLast(options.to)) { - toRect.cy = lastRect.y + lastRect.h + 50; - } - var dragInterval = setInterval(function loop() { - var progress = (new Date().getTime() - startTime) / duration; - var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft; - var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop; - var overEl = fromEl.ownerDocument.elementFromPoint(x, y); - - simulateEvent(overEl, 'mousemove', { - clientX: x, - clientY: y - }); - - if (progress >= 1) { - options.ondragend && options.ondragend(); - simulateEvent(toEl, 'mouseup'); - clearInterval(dragInterval); - window.SIMULATE_DRAG_ACTIVE = 0; - } - }, 100); - - return { - target: fromEl, - fromList: fromEl.parentNode, - toList: toEl.parentNode - }; - } - - // Export - window.simulateEvent = simulateEvent; - window.simulateDrag = simulateDrag; -})(); + if (progress >= 1) { + if (options.ondragend) options.ondragend(); + simulateEvent(toEl, 'mouseup'); + clearInterval(dragInterval); + window.SIMULATE_DRAG_ACTIVE = 0; + } + }, 100); + + return { + target: fromEl, + fromList: fromEl.parentNode, + toList: toEl.parentNode, + }; +} diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js index 059e6c628b3..19c9efe7fbd 100644 --- a/app/assets/javascripts/user.js +++ b/app/assets/javascripts/user.js @@ -1,5 +1,6 @@ /* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */ -/* global Cookies */ + +import Cookies from 'js-cookie'; ((global) => { global.User = class { diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js index 99419e85b20..fa078b48bf8 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,60 +1,27 @@ -/* global Cookies */ - -const userCalloutElementName = '.user-callout'; -const closeButton = '.close-user-callout'; -const userCalloutBtn = '.user-callout-btn'; -const userCalloutSvgAttrName = 'callout-svg'; +import Cookies from 'js-cookie'; const USER_CALLOUT_COOKIE = 'user_callout_dismissed'; -const USER_CALLOUT_TEMPLATE = ` - <div class="bordered-box landing content-block"> - <button class="btn btn-default close close-user-callout" type="button"> - <i class="fa fa-times dismiss-icon"></i> - </button> - <div class="row"> - <div class="col-sm-3 col-xs-12 svg-container"> - </div> - <div class="col-sm-8 col-xs-12 inner-content"> - <h4> - Customize your experience - </h4> - <p> - Change syntax themes, default project pages, and more in preferences. - </p> - <a class="btn user-callout-btn" href="/profile/preferences">Check it out</a> - </div> - </div> -</div>`; - -class UserCallout { +export default class UserCallout { constructor() { this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); - this.userCalloutBody = $(userCalloutElementName); - this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName); - $(userCalloutElementName).removeAttr(userCalloutSvgAttrName); + this.userCalloutBody = $('.user-callout'); this.init(); } init() { - const $template = $(USER_CALLOUT_TEMPLATE); if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') { - $template.find('.svg-container').append(this.userCalloutSvg); - this.userCalloutBody.append($template); - $template.find(closeButton).on('click', e => this.dismissCallout(e)); - $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e)); - } else { - this.userCalloutBody.remove(); + $('.js-close-callout').on('click', e => this.dismissCallout(e)); } } dismissCallout(e) { - Cookies.set(USER_CALLOUT_COOKIE, 'true'); const $currentTarget = $(e.currentTarget); - if ($currentTarget.hasClass('close-user-callout')) { + + Cookies.set(USER_CALLOUT_COOKIE, 'true'); + + if ($currentTarget.hasClass('close')) { this.userCalloutBody.remove(); } } } - -module.exports = UserCallout; diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js index 465618e3d53..5db0d936ad8 100644 --- a/app/assets/javascripts/user_tabs.js +++ b/app/assets/javascripts/user_tabs.js @@ -1,4 +1,4 @@ -/* 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 */ +/* 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 @@ -82,8 +82,19 @@ content on the Users#show page. } bindEvents() { - return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') + 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(); + this.loadTab($(e.target).attr('href'), this.getCurrentAction()); } tabShown(event) { @@ -119,7 +130,7 @@ content on the Users#show page. complete: () => this.toggleLoading(false), dataType: 'json', type: 'GET', - url: `${source}.json`, + url: source, success: (data) => { const tabSelector = `div#${action}`; this.$parentEl.find(tabSelector).html(data.html); @@ -153,6 +164,10 @@ content on the Users#show page. }, 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/users_select.js b/app/assets/javascripts/users_select.js index eb897e9dfe9..48e20cf501f 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,8 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, no-param-reassign */ -/* global Vue */ /* global Issuable */ /* global ListUser */ +import Vue from 'vue'; + (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, slice = [].slice; diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js index aaebf29d8ae..58b8db4d519 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/async_button.js +++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js @@ -83,6 +83,7 @@ export default { :class="buttonClass" :title="title" :aria-label="title" + data-container="body" data-placement="top" :disabled="isLoading"> <i :class="iconClass" aria-hidden="true"/> diff --git a/app/assets/javascripts/vue_pipelines_index/components/empty_state.js b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js new file mode 100644 index 00000000000..56b4858f4b4 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/empty_state.js @@ -0,0 +1,33 @@ +import pipelinesEmptyStateSVG from 'empty_states/icons/_pipelines_empty.svg'; + +export default { + props: { + helpPagePath: { + type: String, + required: true, + }, + }, + + template: ` + <div class="row empty-state"> + <div class="col-xs-12"> + <div class="svg-content"> + ${pipelinesEmptyStateSVG} + </div> + </div> + + <div class="col-xs-12 text-center"> + <div class="text-content"> + <h4>Build with confidence</h4> + <p> + Continous Integration can help catch bugs by running your tests automatically, + while Continuous Deployment can help you deliver code to your product environment. + </p> + <a :href="helpPagePath" class="btn btn-info"> + Get started with Pipelines + </a> + </div> + </div> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/error_state.js b/app/assets/javascripts/vue_pipelines_index/components/error_state.js new file mode 100644 index 00000000000..e5d228bddf8 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/error_state.js @@ -0,0 +1,19 @@ +import pipelinesErrorStateSVG from 'empty_states/icons/_pipelines_failed.svg'; + +export default { + template: ` + <div class="row empty-state js-pipelines-error-state"> + <div class="col-xs-12"> + <div class="svg-content"> + ${pipelinesErrorStateSVG} + </div> + </div> + + <div class="col-xs-12 text-center"> + <div class="text-content"> + <h4>The API failed to fetch the pipelines.</h4> + </div> + </div> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js new file mode 100644 index 00000000000..6aa10531034 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/nav_controls.js @@ -0,0 +1,52 @@ +export default { + props: { + newPipelinePath: { + type: String, + required: true, + }, + + hasCiEnabled: { + type: Boolean, + required: true, + }, + + helpPagePath: { + type: String, + required: true, + }, + + ciLintPath: { + type: String, + required: true, + }, + + canCreatePipeline: { + type: Boolean, + required: true, + }, + }, + + template: ` + <div class="nav-controls"> + <a + v-if="canCreatePipeline" + :href="newPipelinePath" + class="btn btn-create"> + Run Pipeline + </a> + + <a + v-if="!hasCiEnabled" + :href="helpPagePath" + class="btn btn-info"> + Get started with Pipelines + </a> + + <a + :href="ciLintPath" + class="btn btn-default"> + CI Lint + </a> + </div> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js new file mode 100644 index 00000000000..1626ae17a30 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js @@ -0,0 +1,72 @@ +export default { + props: { + scope: { + type: String, + required: true, + }, + + count: { + type: Object, + required: true, + }, + + paths: { + type: Object, + required: true, + }, + }, + + mounted() { + $(document).trigger('init.scrolling-tabs'); + }, + + template: ` + <ul class="nav-links scrolling-tabs"> + <li + class="js-pipelines-tab-all" + :class="{ 'active': scope === 'all'}"> + <a :href="paths.allPath"> + All + <span class="badge js-totalbuilds-count"> + {{count.all}} + </span> + </a> + </li> + <li class="js-pipelines-tab-pending" + :class="{ 'active': scope === 'pending'}"> + <a :href="paths.pendingPath"> + Pending + <span class="badge"> + {{count.pending}} + </span> + </a> + </li> + <li class="js-pipelines-tab-running" + :class="{ 'active': scope === 'running'}"> + <a :href="paths.runningPath"> + Running + <span class="badge"> + {{count.running}} + </span> + </a> + </li> + <li class="js-pipelines-tab-finished" + :class="{ 'active': scope === 'finished'}"> + <a :href="paths.finishedPath"> + Finished + <span class="badge"> + {{count.finished}} + </span> + </a> + </li> + <li class="js-pipelines-tab-branches" + :class="{ 'active': scope === 'branches'}"> + <a :href="paths.branchesPath">Branches</a> + </li> + <li class="js-pipelines-tab-tags" + :class="{ 'active': scope === 'tags'}"> + <a :href="paths.tagsPath">Tags</a> + </li> + </ul> + `, +}; diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js index 4bb2b048884..12d80768646 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js +++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js @@ -38,6 +38,14 @@ export default { new Flash('An error occured while making the request.'); }); }, + + isActionDisabled(action) { + if (action.playable === undefined) { + return false; + } + + return !action.playable; + }, }, template: ` @@ -51,16 +59,23 @@ export default { aria-label="Manual job" :disabled="isLoading"> ${playIconSvg} - <i class="fa fa-caret-down" aria-hidden="true"></i> - <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> + <i + class="fa fa-caret-down" + aria-hidden="true" /> + <i + v-if="isLoading" + class="fa fa-spinner fa-spin" + aria-hidden="true" /> </button> <ul class="dropdown-menu dropdown-menu-align-right"> <li v-for="action in actions"> <button type="button" - class="js-pipeline-action-link no-btn" - @click="onClickAction(action.path)"> + class="js-pipeline-action-link no-btn btn" + @click="onClickAction(action.path)" + :class="{ 'disabled': isActionDisabled(action) }" + :disabled="isActionDisabled(action)"> ${playIconSvg} <span>{{action.name}}</span> </button> diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js index 3555040d60f..f18e2dfadaf 100644 --- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js +++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js @@ -21,6 +21,7 @@ export default { <li v-for="artifact in artifacts"> <a rel="nofollow" + download :href="artifact.path"> <i class="fa fa-download" aria-hidden="true"></i> <span>Download {{artifact.name}} artifacts</span> diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js index b4e2d3a1143..48f9181a8d9 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js +++ b/app/assets/javascripts/vue_pipelines_index/index.js @@ -1,28 +1,22 @@ +import Vue from 'vue'; import PipelinesStore from './stores/pipelines_store'; import PipelinesComponent from './pipelines'; import '../vue_shared/vue_resource_interceptor'; -const Vue = window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); - $(() => new Vue({ - el: document.querySelector('.vue-pipelines-index'), + el: document.querySelector('#pipelines-list-vue'), data() { - const project = document.querySelector('.pipelines'); const store = new PipelinesStore(); return { store, - endpoint: project.dataset.url, }; }, components: { 'vue-pipelines': PipelinesComponent, }, template: ` - <vue-pipelines - :endpoint="endpoint" - :store="store" /> + <vue-pipelines :store="store" /> `, })); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js index f389e5e4950..9bdc232b7da 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js @@ -1,19 +1,15 @@ -/* global Flash */ -/* eslint-disable no-new */ -import '~/flash'; import Vue from 'vue'; import PipelinesService from './services/pipelines_service'; import eventHub from './event_hub'; import PipelinesTableComponent from '../vue_shared/components/pipelines_table'; import TablePaginationComponent from '../vue_shared/components/table_pagination'; +import EmptyState from './components/empty_state'; +import ErrorState from './components/error_state'; +import NavigationTabs from './components/navigation_tabs'; +import NavigationControls from './components/nav_controls'; export default { props: { - endpoint: { - type: String, - required: true, - }, - store: { type: Object, required: true, @@ -23,17 +19,109 @@ export default { components: { 'gl-pagination': TablePaginationComponent, 'pipelines-table-component': PipelinesTableComponent, + 'empty-state': EmptyState, + 'error-state': ErrorState, + 'navigation-tabs': NavigationTabs, + 'navigation-controls': NavigationControls, }, data() { + const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; + return { + endpoint: pipelinesData.endpoint, + cssClass: pipelinesData.cssClass, + helpPagePath: pipelinesData.helpPagePath, + newPipelinePath: pipelinesData.newPipelinePath, + canCreatePipeline: pipelinesData.canCreatePipeline, + allPath: pipelinesData.allPath, + pendingPath: pipelinesData.pendingPath, + runningPath: pipelinesData.runningPath, + finishedPath: pipelinesData.finishedPath, + branchesPath: pipelinesData.branchesPath, + tagsPath: pipelinesData.tagsPath, + hasCi: pipelinesData.hasCi, + ciLintPath: pipelinesData.ciLintPath, state: this.store.state, apiScope: 'all', pagenum: 1, - pageRequest: false, + isLoading: false, + hasError: false, }; }, + computed: { + canCreatePipelineParsed() { + return gl.utils.convertPermissionToBoolean(this.canCreatePipeline); + }, + + scope() { + const scope = gl.utils.getParameterByName('scope'); + return scope === null ? 'all' : scope; + }, + + shouldRenderErrorState() { + return this.hasError && !this.isLoading; + }, + + /** + * The empty state should only be rendered when the request is made to fetch all pipelines + * and none is returned. + * + * @return {Boolean} + */ + shouldRenderEmptyState() { + return !this.isLoading && + !this.hasError && + !this.state.pipelines.length && + (this.scope === 'all' || this.scope === null); + }, + + /** + * When a specific scope does not have pipelines we render a message. + * + * @return {Boolean} + */ + shouldRenderNoPipelinesMessage() { + return !this.isLoading && + !this.hasError && + !this.state.pipelines.length && + this.scope !== 'all' && + this.scope !== null; + }, + + shouldRenderTable() { + return !this.hasError && + !this.isLoading && this.state.pipelines.length; + }, + + /** + * Pagination should only be rendered when there is more than one page. + * + * @return {Boolean} + */ + shouldRenderPagination() { + return !this.isLoading && + this.state.pipelines.length && + this.state.pageInfo.total > this.state.pageInfo.perPage; + }, + + hasCiEnabled() { + return this.hasCi !== undefined; + }, + + paths() { + return { + allPath: this.allPath, + pendingPath: this.pendingPath, + finishedPath: this.finishedPath, + runningPath: this.runningPath, + branchesPath: this.branchesPath, + tagsPath: this.tagsPath, + }; + }, + }, + created() { this.service = new PipelinesService(this.endpoint); @@ -69,7 +157,7 @@ export default { const pageNumber = gl.utils.getParameterByName('page') || this.pagenum; const scope = gl.utils.getParameterByName('scope') || this.apiScope; - this.pageRequest = true; + this.isLoading = true; return this.service.getPipelines(scope, pageNumber) .then(resp => ({ headers: resp.headers, @@ -81,41 +169,78 @@ export default { this.store.storePagination(response.headers); }) .then(() => { - this.pageRequest = false; + this.isLoading = false; }) .catch(() => { - this.pageRequest = false; - new Flash('An error occurred while fetching the pipelines, please reload the page again.'); + this.hasError = true; + this.isLoading = false; }); }, }, - template: ` - <div> - <div class="pipelines realtime-loading" v-if="pageRequest"> - <i class="fa fa-spinner fa-spin" aria-hidden="true"></i> - </div> - <div class="blank-state blank-state-no-icon" - v-if="!pageRequest && state.pipelines.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - No pipelines to show - </h2> + template: ` + <div :class="cssClass"> + + <div + class="top-area scrolling-tabs-container inner-page-scroll-tabs" + v-if="!isLoading && !shouldRenderEmptyState"> + <div class="fade-left"> + <i class="fa fa-angle-left" aria-hidden="true"></i> + </div> + <div class="fade-right"> + <i class="fa fa-angle-right" aria-hidden="true"></i> + </div> + <navigation-tabs + :scope="scope" + :count="state.count" + :paths="paths" /> + + <navigation-controls + :new-pipeline-path="newPipelinePath" + :has-ci-enabled="hasCiEnabled" + :help-page-path="helpPagePath" + :ciLintPath="ciLintPath" + :can-create-pipeline="canCreatePipelineParsed " /> </div> - <div class="table-holder" v-if="!pageRequest && state.pipelines.length"> - <pipelines-table-component - :pipelines="state.pipelines" - :service="service"/> + <div class="content-list pipelines"> + + <div + class="realtime-loading" + v-if="isLoading"> + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" /> + </div> + + <empty-state + v-if="shouldRenderEmptyState" + :help-page-path="helpPagePath" /> + + <error-state v-if="shouldRenderErrorState" /> + + <div + class="blank-state blank-state-no-icon" + v-if="shouldRenderNoPipelinesMessage"> + <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2> + </div> + + <div + class="table-holder" + v-if="shouldRenderTable"> + + <pipelines-table-component + :pipelines="state.pipelines" + :service="service"/> + </div> + + <gl-pagination + v-if="shouldRenderPagination" + :pagenum="pagenum" + :change="change" + :count="state.count.all" + :pageInfo="state.pageInfo"/> </div> - - <gl-pagination - v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage" - :pagenum="pagenum" - :change="change" - :count="state.count.all" - :pageInfo="state.pageInfo" - > - </gl-pagination> </div> `, }; diff --git a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js index 7ac10086a55..377ec8ba2cc 100644 --- a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js +++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js @@ -1,5 +1,5 @@ /* eslint-disable no-underscore-dangle*/ -import '../../vue_realtime_listener'; +import VueRealtimeListener from '../../vue_realtime_listener'; export default class PipelinesStore { constructor() { @@ -56,6 +56,6 @@ export default class PipelinesStore { const removeIntervals = () => clearInterval(this.timeLoopInterval); const startIntervals = () => startTimeLoops(); - gl.VueRealtimeListener(removeIntervals, startIntervals); + VueRealtimeListener(removeIntervals, startIntervals); } } diff --git a/app/assets/javascripts/vue_realtime_listener/index.js b/app/assets/javascripts/vue_realtime_listener/index.js index 30f6680a673..4ddb2f975b0 100644 --- a/app/assets/javascripts/vue_realtime_listener/index.js +++ b/app/assets/javascripts/vue_realtime_listener/index.js @@ -1,29 +1,9 @@ -/* eslint-disable no-param-reassign */ - -((gl) => { - gl.VueRealtimeListener = (removeIntervals, startIntervals) => { - const removeAll = () => { - removeIntervals(); - window.removeEventListener('beforeunload', removeIntervals); - window.removeEventListener('focus', startIntervals); - window.removeEventListener('blur', removeIntervals); - document.removeEventListener('beforeunload', removeAll); - }; - - window.addEventListener('beforeunload', removeIntervals); - window.addEventListener('focus', startIntervals); - window.addEventListener('blur', removeIntervals); - document.addEventListener('beforeunload', removeAll); - - // add removeAll methods to stack - const stack = gl.VueRealtimeListener.reset; - gl.VueRealtimeListener.reset = () => { - gl.VueRealtimeListener.reset = stack; - removeAll(); - stack(); - }; - }; - - // remove all event listeners and intervals - gl.VueRealtimeListener.reset = () => undefined; // noop -})(window.gl || (window.gl = {})); +export default (removeIntervals, startIntervals) => { + window.removeEventListener('focus', startIntervals); + window.removeEventListener('blur', removeIntervals); + window.removeEventListener('onbeforeload', removeIntervals); + + window.addEventListener('focus', startIntervals); + window.addEventListener('blur', removeIntervals); + window.addEventListener('onbeforeload', removeIntervals); +}; diff --git a/app/assets/javascripts/vue_shared/common_vue.js b/app/assets/javascripts/vue_shared/common_vue.js new file mode 100644 index 00000000000..eb2a6071fda --- /dev/null +++ b/app/assets/javascripts/vue_shared/common_vue.js @@ -0,0 +1,6 @@ +import Vue from 'vue'; +import './vue_resource_interceptor'; + +if (process.env.NODE_ENV !== 'production') { + Vue.config.productionTip = false; +} diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js index b9cd28f6249..ebb14912b00 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.js +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js @@ -3,8 +3,8 @@ const UI_LIMIT = 6; const SPREAD = '...'; const PREV = 'Prev'; const NEXT = 'Next'; -const FIRST = '<< First'; -const LAST = 'Last >>'; +const FIRST = 'ยซ First'; +const LAST = 'Last ยป'; export default { props: { diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index f1c1e553b16..d5f87588c28 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -1,20 +1,23 @@ -/* eslint-disable no-param-reassign, no-plusplus */ import Vue from 'vue'; import VueResource from 'vue-resource'; Vue.use(VueResource); +// Maintain a global counter for active requests +// see: spec/support/wait_for_vue_resource.rb Vue.http.interceptors.push((request, next) => { - Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; + window.activeVueResources = window.activeVueResources || 0; + window.activeVueResources += 1; next(() => { - Vue.activeResources--; + window.activeVueResources -= 1; }); }); +// Inject CSRF token so we don't break any tests. Vue.http.interceptors.push((request, next) => { - // needed in order to not break the tests. if ($.rails) { + // eslint-disable-next-line no-param-reassign request.headers['X-CSRF-Token'] = $.rails.csrfToken(); } next(); diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 546718ddaf8..1ae144fb471 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -92,6 +92,10 @@ .award-menu-holder { display: inline-block; position: relative; + + .tooltip { + white-space: nowrap; + } } .award-control { @@ -124,6 +128,10 @@ &:focus { outline: 0; } + + .award-control-icon { + margin: 0; + } } &.is-loading { @@ -153,6 +161,7 @@ .award-control-icon { color: $border-gray-normal; margin-top: 1px; + padding: 0 2px; } .award-control-text { diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 9a4129cdc8d..52425262925 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -292,6 +292,10 @@ } @media(min-width: $screen-xs-max) { + &.merge-requests .text-content { + margin-top: 40px; + } + &.labels .text-content { margin-top: 70px; } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index cda46223492..4369ae78bde 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -68,23 +68,19 @@ } @mixin btn-green { - @include btn-color($green-light, $border-green-light, $green-normal, $border-green-normal, $green-dark, $border-green-dark, $white-light); + @include btn-color($green-500, $green-600, $green-600, $green-700, $green-700, $green-800, $white-light); } @mixin btn-blue { - @include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, $white-light); -} - -@mixin btn-blue-medium { - @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, $white-light); + @include btn-color($blue-500, $blue-600, $blue-600, $blue-700, $blue-700, $blue-800, $white-light); } @mixin btn-orange { - @include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, $white-light); + @include btn-color($orange-500, $orange-600, $orange-600, $orange-700, $orange-700, $orange-800, $white-light); } @mixin btn-red { - @include btn-color($red-light, $border-red-light, $red-normal, $border-red-normal, $red-dark, $border-red-dark, $white-light); + @include btn-color($red-500, $red-600, $red-600, $red-700, $red-700, $red-800, $white-light); } @mixin btn-gray { @@ -145,11 +141,11 @@ &.btn-new, &.btn-create, &.btn-save { - @include btn-outline($white-light, $border-green-light, $border-green-light, $green-light, $white-light, $border-green-light, $green-normal, $border-green-normal); + @include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700); } &.btn-remove { - @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal); + @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } } @@ -157,11 +153,8 @@ @include btn-gray; } - &.btn-primary { - @include btn-blue-medium; - } - &.btn-info, + &.btn-primary, &.btn-register { @include btn-blue; } @@ -171,11 +164,11 @@ } &.btn-close { - @include btn-outline($white-light, $border-orange-light, $border-orange-light, $orange-light, $white-light, $border-orange-light, $orange-normal, $border-orange-normal); + @include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700); } &.btn-spam { - @include btn-outline($white-light, $border-red-light, $border-red-light, $red-light, $white-light, $border-red-light, $red-normal, $border-red-normal); + @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } &.btn-danger, @@ -360,7 +353,7 @@ .btn-inverted { &-secondary { - @include btn-outline($white-light, $border-blue-light, $border-blue-light, $blue-light, $white-light, $border-blue-light, $blue-normal, $border-blue-normal); + @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700); } } @@ -369,3 +362,13 @@ width: 100%; } } + +.btn-blank { + padding: 0; + background: transparent; + border: 0; + + &:focus { + outline: 0; + } +} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 186bb9ac616..2ede47e9de6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -119,6 +119,46 @@ } } +@mixin dropdown-link { + display: block; + position: relative; + padding: 5px 8px; + color: $gl-text-color; + line-height: initial; + text-overflow: ellipsis; + border-radius: 2px; + white-space: nowrap; + overflow: hidden; + + &:hover, + &:focus, + &.is-focused { + background-color: $dropdown-link-hover-bg; + text-decoration: none; + + .badge { + background-color: darken($dropdown-link-hover-bg, 5%); + } + } + + &.dropdown-menu-empty-link { + &.is-focused { + background-color: $dropdown-empty-row-bg; + } + } + + &.dropdown-menu-user-link { + line-height: 16px; + } + + .icon-play { + fill: $gl-text-color-secondary; + margin-right: 6px; + height: 12px; + width: 11px; + } +} + .dropdown-menu, .dropdown-menu-nav { display: none; @@ -178,43 +218,7 @@ } a { - display: block; - position: relative; - padding: 5px 8px; - color: $gl-text-color; - line-height: initial; - text-overflow: ellipsis; - border-radius: 2px; - white-space: nowrap; - overflow: hidden; - - &:hover, - &:focus, - &.is-focused { - background-color: $dropdown-link-hover-bg; - text-decoration: none; - - .badge { - background-color: darken($row-hover, 5%); - } - } - - &.dropdown-menu-empty-link { - &.is-focused { - background-color: $dropdown-empty-row-bg; - } - } - - &.dropdown-menu-user-link { - line-height: 16px; - } - - .icon-play { - fill: $gl-text-color-secondary; - margin-right: 6px; - height: 12px; - width: 11px; - } + @include dropdown-link; } .dropdown-header { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 25d6fbe465a..432024779fd 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -177,37 +177,45 @@ label { } .gl-field-error { - color: $red-normal; + color: $red-500; } .gl-show-field-errors { .gl-field-success-outline { - border: 1px solid $green-normal; + border: 1px solid $green-600; &:focus { - box-shadow: 0 0 0 1px $green-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-normal; + box-shadow: 0 0 0 1px $green-600 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $green-600; border: 0 none; } } .gl-field-error-outline { - border: 1px solid $red-normal; + border: 1px solid $red-500; &:focus { - box-shadow: 0 0 0 1px $red-normal inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error; + box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset, 0 0 4px 0 $gl-field-focus-shadow-error; border: 0 none; } } .gl-field-success-message { - color: $green-normal; + color: $green-600; } .gl-field-error-message { - color: $red-normal; + color: $red-500; } .gl-field-hint { color: $gl-text-color; } } + +@media(max-width: $screen-xs-max) { + .remember-me { + .remember-me-checkbox { + margin-top: 0; + } + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 6660a022260..abb092623c0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -26,7 +26,7 @@ header { padding: 0 16px; z-index: 100; margin-bottom: 0; - height: $header-height; + min-height: $header-height; background-color: $gray-light; border: none; border-bottom: 1px solid $border-color; @@ -48,10 +48,10 @@ header { color: $gl-text-color-secondary; font-size: 18px; padding: 0; - margin: ($header-height - 28) / 2 0; + margin: (($header-height - 28) / 2) 3px; margin-left: 8px; height: 28px; - min-width: 28px; + min-width: 32px; line-height: 28px; text-align: center; @@ -73,21 +73,29 @@ header { background-color: $gray-light; color: $gl-text-color; - .todos-pending-count { - background: darken($todo-alert-blue, 10%); + svg { + fill: $gl-text-color; } } .fa-caret-down { font-size: 14px; } + + svg { + position: relative; + top: 2px; + height: 17px; + // hack to get SVG to line up with FA icons + width: 23px; + fill: $gl-text-color-secondary; + } } .navbar-toggle { color: $nav-toggle-gray; - margin: 6px 0; + margin: 5px 0; border-radius: 0; - position: absolute; right: -10px; padding: 6px 10px; @@ -135,14 +143,12 @@ header { } .header-content { + display: flex; + justify-content: space-between; position: relative; - height: $header-height; + min-height: $header-height; padding-left: 30px; - @media (min-width: $screen-sm-min) { - padding-right: 0; - } - .dropdown-menu { margin-top: -5px; } @@ -165,8 +171,7 @@ header { } .group-name-toggle { - margin: 0 5px; - vertical-align: sub; + margin: 3px 5px; } .group-title { @@ -177,39 +182,32 @@ header { } } + .title-container { + display: flex; + align-items: flex-start; + flex: 1 1 auto; + padding-top: (($header-height - 19) / 2); + overflow: hidden; + } + .title { position: relative; padding-right: 20px; margin: 0; font-size: 18px; - max-width: 385px; + line-height: 22px; display: inline-block; - line-height: $header-height; font-weight: normal; color: $gl-text-color; - overflow: hidden; - text-overflow: ellipsis; vertical-align: top; white-space: nowrap; - &.initializing { - display: none; - } - - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - max-width: 300px; - } - - @media (max-width: $screen-xs-max) { - max-width: 190px; + &.wrap { + white-space: normal; } - @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { - max-width: 428px; - } - - @media (min-width: $screen-lg-min) { - max-width: 685px; + &.initializing { + opacity: 0; } a { @@ -226,10 +224,10 @@ header { border: transparent; background: transparent; position: absolute; + top: 2px; right: 3px; width: 12px; line-height: 19px; - margin-top: (($header-height - 19) / 2); padding: 0; font-size: 10px; text-align: center; @@ -247,15 +245,12 @@ header { } .navbar-collapse { - float: right; + flex: 0 0 auto; border-top: none; - - @media (min-width: $screen-md-min) { - padding: 0; - } + padding: 0; @media (max-width: $screen-xs-max) { - float: none; + flex: 1 1 auto; } } } @@ -265,14 +260,34 @@ header { } .impersonation i { - color: $red-normal; + color: $red-500; } } -.page-sidebar-pinned.right-sidebar-expanded { - @media (max-width: $screen-md-max) { - .header-content .title { - width: 300px; +.navbar-nav { + li { + .badge { + position: inherit; + top: -3px; + font-weight: normal; + margin-left: -12px; + font-size: 11px; + color: $white-light; + padding: 1px 5px 2px; + border-radius: 7px; + box-shadow: 0 1px 0 rgba($gl-header-color, .2); + + &.issues-count { + background-color: $green-500; + } + + &.merge-requests-count { + background-color: $orange-600; + } + + &.todos-count { + background-color: $blue-500; + } } } } diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index db8d231a82a..87667f39ab8 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -1,8 +1,8 @@ .ci-status-icon-success { - color: $gl-success; + color: $green-500; svg { - fill: $gl-success; + fill: $green-500; } } @@ -17,18 +17,18 @@ .ci-status-icon-pending, .ci-status-icon-failed_with_warnings, .ci-status-icon-success_with_warnings { - color: $gl-warning; + color: $orange-500; svg { - fill: $gl-warning; + fill: $orange-500; } } .ci-status-icon-running { - color: $blue-normal; + color: $blue-400; svg { - fill: $blue-normal; + fill: $blue-400; } } diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 46632f15f35..1537b0744cc 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -33,7 +33,7 @@ } &.status-box-open { - background-color: $green-light; + background-color: $green-500; } &.status-box-expired { diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 0a42b17c1f5..20c7bc93c28 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -23,6 +23,10 @@ body { } } +.content-wrapper { + padding-bottom: 100px; +} + .container { padding-top: 0; z-index: 5; @@ -72,28 +76,28 @@ body { /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ .alert-warning { transition: background-color 0.15s, border-color 0.15s; - background-color: lighten($gl-warning, 4%); - border-color: lighten($gl-warning, 4%); + background-color: $orange-500; + border-color: $orange-500; } .alert-warning + .alert-warning { - background-color: $gl-warning; - border-color: $gl-warning; + background-color: $orange-600; + border-color: $orange-600; } .alert-warning + .alert-warning + .alert-warning { - background-color: darken($gl-warning, 4%); - border-color: darken($gl-warning, 4%); + background-color: $orange-700; + border-color: $orange-700; } .alert-warning + .alert-warning + .alert-warning + .alert-warning { - background-color: darken($gl-warning, 8%); - border-color: darken($gl-warning, 8%); + background-color: $orange-800; + border-color: $orange-800; } .alert-warning:only-of-type { - background-color: $gl-warning; - border-color: $gl-warning; + background-color: $orange-500; + border-color: $orange-500; } } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 7adbb0a4188..15dc0aa6a52 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -122,7 +122,7 @@ ul.content-list { } .member-group-link { - color: $blue-normal; + color: $blue-600; } .description { diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index df78bbdea51..b3340d41333 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -52,6 +52,18 @@ } } +@mixin basic-list-stats { + .stats { + float: right; + line-height: $list-text-height; + color: $gl-text-color; + + span { + margin-right: 15px; + } + } +} + @mixin bulleted-list { > ul { list-style-type: disc; diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 205d23b1329..e6d808717f3 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -146,6 +146,10 @@ display: block; } + &.scrolling-tabs { + float: left; + } + li a { padding: 16px 15px 11px; } @@ -416,14 +420,16 @@ .page-with-layout-nav { .right-sidebar { - top: ($header-height * 2) + 2; + top: ($header-height + 1) * 2; } - .build-sidebar { - top: ($header-height * 3) + 3; + &.page-with-sub-nav { + .right-sidebar { + top: ($header-height + 1) * 3; - &.affix { - top: 0; + &.affix { + top: 0; + } } } } @@ -474,3 +480,44 @@ } } } + +.inner-page-scroll-tabs { + position: relative; + + .nav-links { + padding-bottom: 1px; + } + + .fade-right { + @include fade(left, $white-light); + right: 0; + text-align: right; + + .fa { + right: 5px; + } + } + + .fade-left { + @include fade(right, $white-light); + left: 0; + text-align: left; + + .fa { + left: 5px; + } + } + + .fade-right, + .fade-left { + top: 16px; + bottom: auto; + } + + &.is-smaller { + .fade-right, + .fade-left { + top: 11px; + } + } +} diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 40e93032f59..746c9c25620 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -33,7 +33,7 @@ padding-right: 0; @media (min-width: $screen-sm-min) { - .content-wrapper { + &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper { padding-right: $gutter_collapsed_width; } @@ -55,7 +55,7 @@ padding-right: 0; @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { - .content-wrapper { + &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper { padding-right: $gutter_collapsed_width; } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 12a86a64645..e54cc2866a7 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -176,6 +176,10 @@ summary { &.panel-without-border { border: 0; } + + &.panel-without-margin { + margin: 0; + } } .panel-succes .panel-heading, diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 0fc89d5976a..c9f345d24be 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -31,6 +31,7 @@ $border-radius-small: 3px !default; // $text-color: $gl-text-color; $link-color: $gl-link-color; +$link-hover-color: $gl-link-hover-color; //== Typography @@ -73,7 +74,7 @@ $pagination-hover-color: $gl-text-color; $pagination-hover-bg: $row-hover; $pagination-hover-border: $border-color; -$pagination-active-color: $blue-dark; +$pagination-active-color: $blue-600; $pagination-active-bg: $white-light; $pagination-active-border: $border-color; @@ -135,8 +136,8 @@ $well-border: #eee; // //## -$code-color: #c7254e; -$code-bg: #f9f2f4; +$code-color: $red-600; +$code-bg: lighten($red-50, 2%); $kbd-color: $white-light; $kbd-bg: #333; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 6841adb637e..97794a47df8 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -26,27 +26,49 @@ $gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; $gray-darkest: #c4c4c4; -$green-light: #3cbd70; -$green-normal: darken($green-light, $darken-normal-factor); -$green-dark: darken($green-light, $darken-dark-factor); - -$blue-light: #2ea8e5; -$blue-normal: darken($blue-light, $darken-normal-factor); -$blue-dark: darken($blue-light, $darken-dark-factor); - -$blue-medium-light: #3498cb; -$blue-medium: darken($blue-medium-light, $darken-normal-factor); -$blue-medium-dark: darken($blue-medium-light, $darken-dark-factor); - -$blue-light-transparent: rgba(44, 159, 216, 0.05); - -$orange-light: #fc8a51; -$orange-normal: darken($orange-light, $darken-normal-factor); -$orange-dark: darken($orange-light, $darken-dark-factor); - -$red-light: #e52c5a; -$red-normal: darken($red-light, $darken-normal-factor); -$red-dark: darken($red-light, $darken-dark-factor); +$green-50: #e4f5eb; +$green-100: #bae6cc; +$green-200: #8dd5aa; +$green-300: #5fc488; +$green-400: #3cb76f; +$green-500: #1aaa55; +$green-600: #168f48; +$green-700: #12753a; +$green-800: #0e5a2d; +$green-900: #0a4020; + +$blue-50: #e4eff9; +$blue-100: #bcd7f1; +$blue-200: #8fbce8; +$blue-300: #62a1df; +$blue-400: #418cd8; +$blue-500: #1f78d1; +$blue-600: #1b69b6; +$blue-700: #17599c; +$blue-800: #134a81; +$blue-900: #0f3b66; + +$orange-50: #fff2e1; +$orange-100: #fedfb3; +$orange-200: #feca81; +$orange-300: #fdb44f; +$orange-400: #fca429; +$orange-500: #fc9403; +$orange-600: #de7e00; +$orange-700: #c26700; +$orange-800: #a35100; +$orange-900: #853b00; + +$red-50: #fbe7e4; +$red-100: #f4c4bc; +$red-200: #ed9d90; +$red-300: #e67664; +$red-400: #e05842; +$red-500: #db3b21; +$red-600: #c0341d; +$red-700: #a62d19; +$red-800: #8b2615; +$red-900: #711e11; $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); @@ -58,33 +80,11 @@ $border-gray-light: darken($gray-light, $darken-border-factor); $border-gray-normal: darken($gray-normal, $darken-border-factor); $border-gray-dark: darken($white-normal, $darken-border-factor); -$border-green-extra-light: #9adb84; -$border-green-light: darken($green-light, $darken-border-factor); -$border-green-normal: darken($green-normal, $darken-border-factor); -$border-green-dark: darken($green-dark, $darken-border-factor); - -$border-blue-light: darken($blue-light, $darken-border-factor); -$border-blue-normal: darken($blue-normal, $darken-border-factor); -$border-blue-dark: darken($blue-dark, $darken-border-factor); - -$border-orange-light: darken($orange-light, $darken-border-factor); -$border-orange-normal: darken($orange-normal, $darken-border-factor); -$border-orange-dark: darken($orange-dark, $darken-border-factor); - -$border-red-light: darken($red-light, $darken-border-factor); -$border-red-normal: darken($red-normal, $darken-border-factor); -$border-red-dark: darken($red-dark, $darken-border-factor); - -$warning-message-bg: #fbf2d9; -$warning-message-color: #9e8e60; -$warning-message-border: #f0e2bb; - /* * UI elements */ $border-color: #e5e5e5; -$focus-border-color: #3aabf0; -$sidebar-collapsed-icon-color: #999; +$focus-border-color: $blue-300; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -97,10 +97,11 @@ $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-green: #4a2; -$gl-text-red: #d12f19; -$gl-text-orange: #d90; -$gl-link-color: #3777b0; +$gl-text-green: $green-600; +$gl-text-red: $red-500; +$gl-text-orange: $orange-600; +$gl-link-color: $blue-600; +$gl-link-hover-color: $blue-800; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; @@ -117,9 +118,9 @@ $list-text-disabled-color: $gl-text-color-disabled; $list-border-light: #eee; $list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; -$list-warning-row-bg: #fcf8e3; -$list-warning-row-border: #faebcc; -$list-warning-row-color: #8a6d3b; +$list-warning-row-bg: $orange-50; +$list-warning-row-border: $orange-100; +$list-warning-row-color: $orange-700; /* * Markdown @@ -146,24 +147,24 @@ $gl-sidebar-padding: 22px; /* * Misc */ -$row-hover: #f7faff; -$row-hover-border: #b2d7ff; +$row-hover: lighten($blue-50, 2%); +$row-hover-border: $blue-100; $progress-color: #c0392b; $header-height: 50px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; $gl-avatar-size: 40px; -$error-exclamation-point: #e62958; +$error-exclamation-point: $red-500; $border-radius-default: 2px; $settings-icon-size: 18px; -$provider-btn-not-active-color: #4688f1; -$link-underline-blue: #4a8bee; -$active-item-blue: #4a8bee; +$provider-btn-not-active-color: $blue-500; +$link-underline-blue: $blue-500; +$active-item-blue: $blue-500; $layout-link-gray: #7e7c7c; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; -$issue-status-expired: #cea61b; +$issue-status-expired: $orange-500; $issuable-sidebar-color: $gl-text-color-secondary; $show-aside-bg: #eee; $show-aside-color: #777; @@ -192,10 +193,10 @@ $user-mention-color: #2fa0bb; $time-color: #999; $project-member-show-color: #aaa; $gl-promo-color: #aaa; -$error-bg: #c67; -$warning-message-bg: #ffffe6; -$warning-message-border: #ed9; -$warning-message-color: #b90; +$error-bg: $red-400; +$warning-message-bg: $orange-50; +$warning-message-border: $orange-100; +$warning-message-color: $orange-700; $control-group-descr-color: #666; $table-permission-x-bg: #d9edf7; $username-color: #666; @@ -210,30 +211,30 @@ $tanuki-yellow: #fca326; /* * State colors: */ -$gl-primary: $blue-normal; -$gl-success: $green-normal; +$gl-primary: $blue-500; +$gl-success: $green-500; $gl-success-focus: rgba($gl-success, .4); -$gl-info: $blue-normal; -$gl-warning: $orange-normal; -$gl-danger: $red-normal; +$gl-info: $blue-500; +$gl-warning: $orange-500; +$gl-danger: $red-500; $gl-btn-active-background: rgba(0, 0, 0, 0.16); $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background; /* * Commit Diff Colors */ -$added: #63c363; -$deleted: #f77; -$line-added: #ecfdf0; -$line-added-dark: #c7f0d2; -$line-removed: #fbe9eb; -$line-removed-dark: #fac5cd; -$line-number-old: #f9d7dc; -$line-number-new: #ddfbe6; -$line-number-select: #fbf2da; -$line-target-blue: #f6faff; -$line-select-yellow: #fcf8e7; -$line-select-yellow-dark: #f0e2bd; +$added: $green-300; +$deleted: $red-300; +$line-added: $green-50; +$line-added-dark: $green-100; +$line-removed: $red-50; +$line-removed-dark: $red-100; +$line-number-old: lighten($red-100, 5%); +$line-number-new: lighten($green-100, 5%); +$line-number-select: lighten($orange-100, 5%); +$line-target-blue: $blue-50; +$line-select-yellow: $orange-50; +$line-select-yellow-dark: $orange-100; $dark-diff-match-bg: rgba(255, 255, 255, 0.3); $dark-diff-match-color: rgba(255, 255, 255, 0.1); $file-mode-changed: #777; @@ -273,7 +274,7 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%); /* * Filtered Search */ -$dropdown-hover-color: #3b86ff; +$dropdown-hover-color: $blue-400; /* * Buttons @@ -296,10 +297,10 @@ $award-emoji-menu-shadow: rgba(0,0,0,.175); /* * Search Box */ -$search-input-border-color: rgba(#4688f1, .8); +$search-input-border-color: rgba($blue-400, .8); $search-input-focus-shadow-color: $dropdown-input-focus-shadow; $search-input-width: 220px; -$location-badge-active-bg: #4f91f8; +$location-badge-active-bg: $blue-500; $location-icon-color: #e7e9ed; /* @@ -361,18 +362,18 @@ $builds-trace-bg: #111; /* * Callout */ -$callout-danger-bg: #fdf7f7; -$callout-danger-border: #eed3d7; -$callout-danger-color: #b94a48; -$callout-warning-bg: #faf8f0; -$callout-warning-border: #faebcc; -$callout-warning-color: #8a6d3b; -$callout-info-bg: #f4f8fa; -$callout-info-border: #bce8f1; -$callout-info-color: #34789a; -$callout-success-bg: #dff0d8; -$callout-success-border: #5ca64d; -$callout-success-color: #3c763d; +$callout-danger-bg: $red-50; +$callout-danger-border: $red-100; +$callout-danger-color: $red-700; +$callout-warning-bg: $orange-50; +$callout-warning-border: $orange-100; +$callout-warning-color: $orange-700; +$callout-info-bg: $blue-50; +$callout-info-border: $blue-100; +$callout-info-color: $blue-700; +$callout-success-bg: $green-50; +$callout-success-border: $green-100; +$callout-success-color: $green-700; /* * Commit Page @@ -392,7 +393,7 @@ $common-green: $gl-text-green; /* * Editor */ -$editor-cancel-color: #b94a48; +$editor-cancel-color: $red-600; /* * Events @@ -416,10 +417,10 @@ $logs-p-color: #333; * Forms */ $input-danger-bg: #f2dede; -$input-danger-border: #d66; +$input-danger-border: $red-400; $input-group-addon-bg: #f7f8fa; $gl-field-focus-shadow: rgba(0, 0, 0, 0.075); -$gl-field-focus-shadow-error: rgba(210, 40, 82, 0.6); +$gl-field-focus-shadow-error: rgba($red-500, 0.6); /* * Help @@ -453,14 +454,14 @@ $label-border-radius: 100px; /* * Lint */ -$lint-incorrect-color: red; -$lint-correct-color: #47a447; +$lint-incorrect-color: $red-500; +$lint-correct-color: $green-500; /* * Login */ $login-brand-holder-color: #888; -$login-devise-error-color: #a00; +$login-devise-error-color: $red-700; /* * Nav @@ -474,33 +475,33 @@ $nav-toggle-gray: #666; */ $notify-details: #777; $notify-footer: #777; -$notify-new-file: #090; -$notify-deleted-file: #b00; +$notify-new-file: $green-600; +$notify-deleted-file: $red-700; /* * Projects */ $project-option-descr-color: #54565b; $project-breadcrumb-color: #999; -$project-private-forks-notice-odd: #2aa056; +$project-private-forks-notice-odd: $green-600; $project-network-controls-color: #888; /* * Runners */ -$runner-state-shared-bg: #32b186; -$runner-state-specific-bg: #3498db; -$runner-status-online-color: $green-normal; +$runner-state-shared-bg: $green-400; +$runner-state-specific-bg: $blue-400; +$runner-status-online-color: $green-600; $runner-status-offline-color: $gray-darkest; -$runner-status-paused-color: $red-normal; +$runner-status-paused-color: $red-500; /* Stat Graph */ $stat-graph-common-bg: #f3f3f3; -$stat-graph-area-fill: #1db34f; +$stat-graph-area-fill: $green-500; $stat-graph-axis-fill: #aaa; -$stat-graph-orange-fill: #f17f49; +$stat-graph-orange-fill: $orange-500; $stat-graph-selection-fill: #333; $stat-graph-selection-stroke: #333; @@ -514,7 +515,7 @@ $select2-drop-shadow2: rgba(31, 37, 50, 0.317647); /* * Todo */ -$todo-alert-blue: #428bca; +$todo-alert-blue: $blue-500; $todo-body-pre-color: #777; $todo-body-border: #ddd; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index f9ee33019cd..7c0fc1008d0 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -46,7 +46,7 @@ } .issue-boards-page { - .page-with-sidebar { + .content-wrapper { padding-bottom: 0; } } @@ -72,7 +72,7 @@ @media (min-width: $screen-sm-min) { height: 475px; // Needed for PhantomJS - height: calc(100vh - 220px); + height: calc(100vh - 222px); min-height: 475px; transition: width .2s; @@ -240,8 +240,13 @@ font-size: (14px / $issue-boards-font-size) * 1em; } + .card-assignee { + margin-right: 5px; + } + .avatar { margin-left: 0; + margin-right: 0; } } @@ -296,7 +301,7 @@ } } -.issue-boards-sidebar { +.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar { &.right-sidebar { top: 0; bottom: 0; @@ -487,9 +492,9 @@ right: -3px; top: -3px; width: 17px; - background-color: $blue-light; + background-color: $blue-500; color: $white-light; - border: 1px solid $border-blue-light; + border: 1px solid $blue-600; font-size: 9px; line-height: 15px; border-radius: 50%; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index a24292a7c8c..969fc75c6eb 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -366,9 +366,3 @@ right: 0; margin-top: -17px; } - -@media (min-width: $screen-md-min) { - .sub-nav.build { - width: calc(100% + #{$gutter_width}); - } -} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index da8410eca66..0dad91ba128 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -142,7 +142,9 @@ border: 1px solid $border-gray-dark; border-radius: $border-radius-default; margin-left: 5px; - line-height: 1; + font-size: $gl-font-size; + line-height: $gl-font-size; + outline: none; &:hover { background-color: darken($gray-light, 10%); diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index eab79c2a481..1aa1079903c 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -431,6 +431,21 @@ border-bottom: none; } +.diff-stats-summary-toggler { + padding: 0; + background-color: transparent; + border: 0; + color: $gl-link-color; + transition: color 0.1s linear; + + &:hover, + &:focus { + outline: none; + text-decoration: underline; + color: $gl-link-hover-color; + } +} + // Mobile @media (max-width: 480px) { .diff-title { diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index 4af267403d8..f6b8c8ee2bc 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -1,4 +1,13 @@ .file-editor { + .nav-links { + border-top: 1px solid $border-color; + border-right: 1px solid $border-color; + border-left: 1px solid $border-color; + border-bottom: none; + border-radius: 2px; + background: $gray-normal; + } + #editor { border: none; border-radius: 0; @@ -72,11 +81,7 @@ } .encoding-selector, - .soft-wrap-toggle, - .license-selector, - .gitignore-selector, - .gitlab-ci-yml-selector, - .dockerfile-selector { + .soft-wrap-toggle { display: inline-block; vertical-align: top; font-family: $regular_font; @@ -103,28 +108,9 @@ } } } - - .gitignore-selector, - .license-selector, - .gitlab-ci-yml-selector, - .dockerfile-selector { - .dropdown { - line-height: 21px; - } - - .dropdown-menu-toggle { - vertical-align: top; - width: 220px; - } - } - - .gitlab-ci-yml-selector { - .dropdown-menu-toggle { - width: 250px; - } - } } + @media(max-width: $screen-xs-max){ .file-editor { .file-title { @@ -149,10 +135,7 @@ margin: 3px 0; } - .encoding-selector, - .license-selector, - .gitignore-selector, - .gitlab-ci-yml-selector { + .encoding-selector { display: block; margin: 3px 0; @@ -163,3 +146,104 @@ } } } + +.blob-new-page-title, +.blob-edit-page-title { + margin: 19px 0 21px; + vertical-align: top; + display: inline-block; + + @media(max-width: $screen-sm-max) { + display: block; + margin: 19px 0 12px; + } +} + +.template-selectors-menu { + display: inline-block; + vertical-align: top; + margin: 14px 0 0 16px; + padding: 0 0 0 14px; + border-left: 1px solid $border-color; + + @media(max-width: $screen-sm-max) { + display: block; + width: 100%; + margin: 5px 0; + padding: 0; + border-left: none; + } +} + +.templates-selectors-label { + display: inline-block; + vertical-align: top; + margin-top: 6px; + line-height: 21px; + + @media(max-width: $screen-sm-max) { + display: block; + margin: 5px 0; + } +} + +.template-selector-dropdowns-wrap { + display: inline-block; + margin-left: 8px; + vertical-align: top; + margin: 5px 0 0 8px; + + @media(max-width: $screen-sm-max) { + display: block; + width: 100%; + margin: 0 0 16px; + } + + .license-selector, + .gitignore-selector, + .gitlab-ci-yml-selector, + .dockerfile-selector, + .template-type-selector { + display: inline-block; + vertical-align: top; + font-family: $regular_font; + margin-top: -5px; + + @media(max-width: $screen-sm-max) { + display: block; + width: 100%; + margin: 5px 0; + } + + .dropdown { + line-height: 21px; + } + + .dropdown-menu-toggle { + width: 250px; + vertical-align: top; + + @media(max-width: $screen-sm-max) { + display: block; + width: 100%; + margin: 5px 0; + } + } + + } +} + +.template-selectors-undo-menu { + display: inline-block; + margin: 7px 0 0 10px; + + @media(max-width: $screen-sm-max) { + display: block; + width: 100%; + margin: 20px 0; + } + + button { + margin: -4px 0 0 15px; + } +} diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 73a5da715f2..6faa3794c83 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -18,12 +18,15 @@ .environments-container { .table-holder { width: 100%; - overflow: auto; + + @media (max-width: $screen-sm-max) { + overflow: auto; + } } .table.ci-table { .environments-actions { - min-width: 200px; + min-width: 300px; } .environments-commit, @@ -156,6 +159,16 @@ text { fill: $stat-graph-axis-fill; } + + .label-axis-text, + .text-metric-usage { + fill: $black; + font-weight: 500; + } + + .legend-axis-text { + fill: $black; + } } .x-axis path, @@ -219,3 +232,12 @@ stroke: $black; stroke-width: 1; } + +.environments-actions { + .external-url, + .monitoring-url, + .terminal-button, + .stop-env-link { + width: 38px; + } +} diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 84d21e48463..73a5889867a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -9,16 +9,15 @@ } } -.group-row { - .stats { - float: right; - line-height: $list-text-height; - color: $gl-text-color; +.group-root-path { + max-width: 40vw; + overflow: hidden; + text-overflow: ellipsis; + word-wrap: nowrap; +} - span { - margin-right: 15px; - } - } +.group-row { + @include basic-list-stats; } .ldap-group-links { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4426169ef5a..e84a05e3e9e 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -243,6 +243,10 @@ font-size: 13px; font-weight: normal; } + + .hide-expanded { + display: none; + } } &.right-sidebar-collapsed { @@ -282,10 +286,11 @@ display: block; width: 100%; text-align: center; - padding-bottom: 10px; + margin-bottom: 10px; color: $issuable-sidebar-color; - &:hover { + &:hover, + &:hover .todo-undone { color: $gl-text-color; } @@ -294,6 +299,10 @@ margin-top: 0; } + .todo-undone { + color: $gl-link-color; + } + .author { display: none; } @@ -498,7 +507,7 @@ svg { width: 16px; height: 16px; - fill: $sidebar-collapsed-icon-color; + fill: $issuable-sidebar-color; } &:hover svg { @@ -520,12 +529,12 @@ &.over_estimate { .meter-fill { - background: $red-light; + background: $red-500; } .time-remaining, .compare-value.spent { - color: $red-light; + color: $red-500; } } } @@ -582,3 +591,21 @@ opacity: 0; } } + +.issuable-todo-btn { + .fa-spinner { + display: none; + } + + &.is-loading { + .fa-spinner { + display: inline-block; + } + + &.sidebar-collapsed-icon { + .issuable-todo-inner { + display: none; + } + } + } +} diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index cb7ebd61504..b2f45625a2a 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -46,6 +46,10 @@ ul.related-merge-requests > li { .merge-request-id { flex-shrink: 0; } + + .merge-request-info { + margin-left: 5px; + } } .merge-requests-title, @@ -58,10 +62,6 @@ ul.related-merge-requests > li { display: inline-block; } -.merge-request-info { - margin-left: 5px; -} - .merge-request-status { font-size: 13px; padding: 0 5px; @@ -69,21 +69,17 @@ ul.related-merge-requests > li { height: 20px; border-radius: 3px; line-height: 18px; - border: 1px solid; &.merged { - border-color: darken($blue-normal, 10%); - background: $blue-normal; + background: $blue-500; } &.closed { - border-color: darken($red-normal, 10%); - background: $red-normal; + background: $red-500; } &.open { - border: 1px solid darken($green-normal, 10%); - background: $green-normal; + background: $green-500; } } diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 71ed5b1361a..8249e02b64a 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -85,11 +85,11 @@ } .username .validation-success { - color: $green-normal; + color: $green-600; } .username .validation-error { - color: $red-normal; + color: $red-500; } } } diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 5a9f199fb34..35cefd449f1 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -255,7 +255,7 @@ $colors: ( &.saved { .editor { - border-top: solid 2px $border-green-extra-light; + border-top: solid 2px $green-200; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 7c3172421c1..566dcc64802 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -60,7 +60,17 @@ } .modify-merge-commit-link { + padding: 0; + + background-color: transparent; + border: 0; + color: $gl-text-color; + + &:hover, + &:focus { + text-decoration: underline; + } } .merge-param-checkbox { @@ -535,7 +545,7 @@ } .fa-info-circle { - color: $orange-normal; + color: $orange-500; padding-right: 5px; } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 27c47d36818..335e587b8f4 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -52,66 +52,62 @@ } } -.milestone-summary { - .milestone-stat { - white-space: nowrap; - margin-right: 10px; +.milestone-sidebar { + .gutter-toggle { + margin-bottom: 10px; + } - &.with-drilldown { - margin-right: 2px; + .milestone-progress { + .title { + padding-top: 5px; } - } - .remaining-days { - color: $orange-light; + .progress { + height: 6px; + margin: 0; + } } - .milestone-stats-and-buttons { - display: flex; - justify-content: flex-start; - flex-wrap: wrap; + .collapsed-milestone-date { + font-size: 12px; + } - @media (min-width: $screen-xs-min) { - justify-content: space-between; - flex-wrap: nowrap; - } + .milestone-date { + display: block; } - .milestone-progress-buttons { - order: 1; - margin-top: 10px; + .date-separator { + line-height: 5px; + } - @media (min-width: $screen-xs-min) { - order: 2; - margin-top: 0; - flex-shrink: 0; - } + .remaining-days strong { + font-weight: normal; + } - .btn { - float: left; - margin-right: $btn-side-margin; + .milestone-stat { + float: left; + margin-right: 14px; + } - &:last-child { - margin-right: 0; - } - } + .milestone-stat:last-child { + margin-right: 0; } - .milestone-stats { - order: 2; - width: 100%; - padding: 7px 0; - flex-shrink: 1; + .milestone-progress { + .sidebar-collapsed-icon { + clear: both; + padding: 15px 5px 5px; - @media (min-width: $screen-xs-min) { - // when displayed on one line stats go first, buttons second - order: 1; + .progress { + margin: 5px 0; + } } } - .progress { - width: 100%; - margin: 15px 0; + .right-sidebar-collapsed & { + .reference { + border-top: 1px solid $border-gray-normal; + } } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e238f0865f6..57cf8e136e2 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -243,22 +243,6 @@ ul.notes { } } -.page-sidebar-pinned.right-sidebar-expanded { - @media (max-width: $screen-md-max) { - .note-header { - .note-headline-light { - display: block; - } - - .note-actions { - position: absolute; - right: 0; - top: 0; - } - } - } -} - // Diff code in discussion view .discussion-body .diff-file { .file-title { @@ -426,8 +410,22 @@ ul.notes { } .discussion-toggle-button { + padding: 0; + background-color: transparent; + border: 0; line-height: 20px; font-size: 13px; + transition: color 0.1s linear; + + &:hover { + color: $gl-link-color; + } + + &:focus { + text-decoration: underline; + outline: none; + color: $gl-link-color; + } .fa { margin-right: 3px; @@ -462,17 +460,18 @@ ul.notes { background: $white-light; padding: 1px 5px; font-size: 12px; - color: $gl-link-color; + color: $blue-500; margin-left: -55px; position: absolute; z-index: 10; width: 23px; height: 23px; - border: 1px solid $border-color; + border: 1px solid $blue-500; transition: transform .1s ease-in-out; &:hover { - background: $gl-info; + background: $blue-500; + border-color: $blue-600; color: $white-light; transform: scale(1.15); } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 33b38ca6923..a4fe652b52f 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -2,6 +2,7 @@ .realtime-loading { font-size: 40px; text-align: center; + margin: 0 auto; } .stage { @@ -13,9 +14,16 @@ white-space: nowrap; } + .empty-state { + margin: 5% auto 0; + } + .table-holder { width: 100%; - overflow: auto; + + @media (max-width: $screen-sm-max) { + overflow: auto; + } } .commit-title { @@ -99,8 +107,6 @@ @media (max-width: $screen-md-max) { .content-list { - &.pipelines, - &.environments-container, &.builds-content-list { width: 100%; overflow: auto; @@ -667,51 +673,71 @@ // Dropdown button animation in mini pipeline graph &.ci-status-icon-success { - border-color: $gl-success; - color: $gl-success; + border-color: $green-500; + color: $green-500; &:hover, &:focus, &:active { - background-color: rgba($gl-success, 0.1); - border-color: $gl-success; + background-color: $green-50; + border-color: $green-600; + color: $green-600; + + svg { + fill: $green-600; + } } } &.ci-status-icon-failed { - border-color: $gl-danger; - color: $gl-danger; + border-color: $red-500; + color: $red-500; &:hover, &:focus, &:active { - background-color: rgba($gl-danger, 0.1); - border-color: $gl-danger; + background-color: $red-50; + border-color: $red-600; + color: $red-600; + + svg { + fill: $red-600; + } } } &.ci-status-icon-pending, &.ci-status-icon-success_with_warnings { - border-color: $gl-warning; - color: $gl-warning; + border-color: $orange-500; + color: $orange-500; &:hover, &:focus, &:active { - background-color: rgba($gl-warning, 0.1); - border-color: $gl-warning; + background-color: $orange-50; + border-color: $orange-600; + color: $orange-600; + + svg { + fill: $orange-600; + } } } &.ci-status-icon-running { - border-color: $blue-normal; - color: $blue-normal; + border-color: $blue-400; + color: $blue-400; &:hover, &:focus, &:active { - background-color: rgba($blue-normal, 0.1); - border-color: $blue-normal; + background-color: $blue-50; + border-color: $blue-600; + color: $blue-600; + + svg { + fill: $blue-600; + } } } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 1a983d8c9ef..703c5fc8869 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -74,7 +74,6 @@ display: inline; a { - color: $blue-dark; text-decoration: none; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 7c93ee8af18..960875674cc 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -459,34 +459,13 @@ a.deploy-project-label { flex-wrap: wrap; .btn { - margin: 0 10px 10px 0; padding: 8px; + margin-left: 10px; } > div { + margin-bottom: 10px; padding-left: 0; - - &:last-child { - margin-bottom: 0; - - .btn { - margin-right: 0; - } - } - } - } -} - -.page-sidebar-pinned { - .project-stats .nav > li.right { - @media (min-width: $screen-lg-min) { - float: none; - } - } - - .download-button { - @media (min-width: $screen-lg-min) { - margin-left: 0; } } } @@ -582,54 +561,65 @@ pre.light-well { /* * Projects list rendered on dashboard and user page */ - .projects-list { @include basic-list; + display: flex; + flex-direction: column; - .project-row { - border-color: $white-normal; + // Disable Flexbox for admin page + &.admin-projects { + display: block; - .project-full-name { - @include str-truncated; - - @media (max-width: $screen-xs-max) { - max-width: 50%; - } + .project-row { + display: block; } + } - .controls { - line-height: $list-text-height; + .project-row { + display: flex; + align-items: center; + @include basic-list-stats; + } - .badge { - @media (max-width: $screen-xs-max) { - display: none; - } - } + h3 { + font-size: $gl-font-size; + } - a:hover { - text-decoration: none; - } + a { + color: $gl-text-color; + } - > span { - margin-left: 10px; - } + .avatar-container, + .controls { + flex: 0 0 auto; + } - svg { - position: relative; - top: 2px; - } - } + .avatar-container { + align-self: flex-start; + } - .description p { - @media (max-width: $screen-xs-max) { - max-width: 50%; - } + .project-details { + min-width: 0; + + p, + .commit-row-message { + @include str-truncated(100%); + margin-bottom: 0; } } - .bottom { - padding-top: $gl-padding; - padding-bottom: 0; + .controls { + margin-left: auto; + } + + .ci-status-link { + display: inline-block; + line-height: 17px; + vertical-align: middle; + + &:hover { + text-decoration: none; + } } } @@ -745,6 +735,15 @@ pre.light-well { } } +.create-new-protected-branch-button { + @include dropdown-link; + + width: 100%; + background-color: transparent; + border: 0; + text-align: left; +} + .protected-branches-list, .protected-tags-list { margin-bottom: 30px; diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss index bed6470dbd3..23a9c2ada80 100644 --- a/app/assets/stylesheets/pages/sherlock.scss +++ b/app/assets/stylesheets/pages/sherlock.scss @@ -28,6 +28,6 @@ table .sherlock-code { } .sherlock-line-samples-table .slow { - color: $red-light; + color: $red-500; font-weight: bold; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 6f31d4ed789..4a284247143 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -21,42 +21,41 @@ &.ci-failed, &.ci-failed_with_warnings { - color: $gl-danger; - border-color: $gl-danger; + color: $red-500; + border-color: $red-500; &:not(span):hover { - background-color: rgba($gl-danger, .07); + background-color: $red-50; + color: $red-600; + border-color: $red-600; + + svg { + fill: $red-600; + } } svg { - fill: $gl-danger; + fill: $red-500; } } &.ci-success, &.ci-success_with_warnings { - color: $gl-success; - border-color: $gl-success; + color: $green-600; + border-color: $green-500; &:not(span):hover { - background-color: rgba($gl-success, .07); - } - - svg { - fill: $gl-success; - } - } - - &.ci-info { - color: $gl-info; - border-color: $gl-info; + background-color: $green-50; + color: $green-700; + border-color: $green-600; - &:not(span):hover { - background-color: rgba($gl-info, .07); + svg { + fill: $green-600; + } } svg { - fill: $gl-info; + fill: $green-500; } } @@ -75,28 +74,41 @@ } &.ci-pending { - color: $gl-warning; - border-color: $gl-warning; + color: $orange-600; + border-color: $orange-500; &:not(span):hover { - background-color: rgba($gl-warning, .07); + background-color: $orange-50; + color: $orange-700; + border-color: $orange-600; + + svg { + fill: $orange-600; + } } svg { - fill: $gl-warning; + fill: $orange-500; } } + &.ci-info, &.ci-running { - color: $blue-normal; - border-color: $blue-normal; + color: $blue-500; + border-color: $blue-500; &:not(span):hover { - background-color: rgba($blue-normal, .07); + background-color: $blue-50; + color: $blue-600; + border-color: $blue-600; + + svg { + fill: $blue-600; + } } svg { - fill: $blue-normal; + fill: $blue-500; } } diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 5f0aede4f5e..a39815319f3 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -3,25 +3,6 @@ * */ -.navbar-nav { - li { - .badge.todos-pending-count { - position: inherit; - top: -6px; - margin-top: -5px; - font-weight: normal; - background: $todo-alert-blue; - margin-left: -17px; - font-size: 11px; - color: $white-light; - padding: 3px; - padding-top: 1px; - padding-bottom: 1px; - border-radius: 3px; - } - } -} - .todos-list > .todo { // workaround because we cannot use border-colapse border-top: 1px solid transparent; @@ -47,6 +28,7 @@ .todo-avatar, .todo-actions { + @include transition(opacity); -webkit-flex: 0 0 auto; flex: 0 0 auto; } @@ -67,21 +49,34 @@ flex: 0 1 100%; min-width: 0; } -} -.todos-list > .todo.todo-pending.done-reversible { - background-color: $gray-light; + &.todo-pending.done-reversible { + background-color: $white-light; - &:hover { - border-color: $border-color; - } + &:hover { + border-color: $white-dark; + background-color: $gray-light; - .title { - font-weight: normal; + .todo-avatar, + .todo-item { + opacity: .6; + } + } + + .todo-avatar, + .todo-item { + opacity: .2; + } + + .btn { + background-color: $gray-light; + } } } .todo-item { + @include transition(opacity); + .todo-title { display: flex; diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb index 5055c318a5f..dc9a6df5f75 100644 --- a/app/controllers/admin/abuse_reports_controller.rb +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -1,6 +1,7 @@ class Admin::AbuseReportsController < Admin::ApplicationController def index @abuse_reports = AbuseReport.order(id: :desc).page(params[:page]) + @abuse_reports.includes(:reporter, :user) end def destroy diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8d831ffdd70..515d8e1523b 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -45,15 +45,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def application_setting_params - restricted_levels = params[:application_setting][:restricted_visibility_levels] - if restricted_levels.nil? - params[:application_setting][:restricted_visibility_levels] = [] - else - restricted_levels.map! do |level| - level.to_i - end - end - import_sources = params[:application_setting][:import_sources] if import_sources.nil? params[:application_setting][:import_sources] = [] @@ -143,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :unique_ips_limit_enabled, :version_check_enabled, :terminal_max_session_time, + :polling_interval_multiplier, disabled_oauth_sign_in_sources: [], import_sources: [], diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb index c09095b9849..5f90ad7137d 100644 --- a/app/controllers/admin/background_jobs_controller.rb +++ b/app/controllers/admin/background_jobs_controller.rb @@ -1,7 +1,7 @@ class Admin::BackgroundJobsController < Admin::ApplicationController def show - ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) - @sidekiq_processes = ps_output.split("\n").grep(/sidekiq/) + ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) + @sidekiq_processes = ps_output.split("\n").grep(/sidekiq \d+\.\d+\.\d+/) @concurrency = Sidekiq.options[:concurrency] end end diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb index d496f08a598..4531657268c 100644 --- a/app/controllers/admin/labels_controller.rb +++ b/app/controllers/admin/labels_controller.rb @@ -16,10 +16,9 @@ class Admin::LabelsController < Admin::ApplicationController end def create - @label = Label.new(label_params) - @label.template = true + @label = Labels::CreateService.new(label_params).execute(template: true) - if @label.save + if @label.persisted? redirect_to admin_labels_url, notice: "Label was created" else render :new @@ -27,7 +26,9 @@ class Admin::LabelsController < Admin::ApplicationController end def update - if @label.update(label_params) + @label = Labels::UpdateService.new(label_params).execute(@label) + + if @label.valid? redirect_to admin_labels_path, notice: 'label was successfully updated.' else render :edit diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 24504685e48..563bcc65bd6 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -95,18 +95,14 @@ class Admin::UsersController < Admin::ApplicationController def create opts = { - force_random_password: true, - password_expires_at: nil + reset_password: true, + skip_confirmation: true } - @user = User.new(user_params.merge(opts)) - @user.created_by_id = current_user.id - @user.generate_password - @user.generate_reset_token - @user.skip_confirmation! + @user = Users::CreateService.new(current_user, user_params.merge(opts)).execute respond_to do |format| - if @user.save + if @user.persisted? format.html { redirect_to [:admin, @user], notice: 'User was successfully created.' } format.json { render json: @user, status: :created, location: @user } else diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b7ce081a5cd..6a6e335d314 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -64,8 +64,11 @@ class ApplicationController < ActionController::Base # This filter handles both private tokens and personal access tokens def authenticate_user_from_private_token! - token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence - user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string) + token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence + + return unless token.present? + + user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) if user && can?(user, :log_in) # Notice we are passing store false, so the user is not diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb index 0a995c45bdf..eb3a623acdd 100644 --- a/app/controllers/concerns/continue_params.rb +++ b/app/controllers/concerns/continue_params.rb @@ -7,6 +7,7 @@ module ContinueParams continue_params = continue_params.permit(:to, :notice, :notice_now) return unless continue_params[:to] && continue_params[:to].start_with?('/') + return if continue_params[:to].start_with?('//') continue_params end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 85ae4985e58..c8a501d7319 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -15,6 +15,9 @@ module IssuableCollections # a new order into the collection. # We cannot use reorder to not mess up the paginated collection. issuable_ids = issuable_collection.map(&:id) + + return {} if issuable_ids.empty? + issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type) issuable_merge_requests_count = diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 096de8032ae..4d7d45787fc 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -7,7 +7,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController @sort = params[:sort] @todos = @todos.page(params[:page]) if @todos.out_of_range? && @todos.total_pages != 0 - redirect_to url_for(params.merge(page: @todos.total_pages)) + redirect_to url_for(params.merge(page: @todos.total_pages, only_path: true)) end end @@ -51,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController private def find_todos - @todos ||= TodosFinder.new(current_user, params.merge(include_associations: true)).execute + @todos ||= TodosFinder.new(current_user, params).execute end def todos_counts diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index c411c21bb80..8b69c18d689 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -10,6 +10,7 @@ class Groups::ApplicationController < ApplicationController unless @group id = params[:group_id] || params[:id] @group = Group.find_by_full_path(id) + @group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute unless @group && can?(current_user, :read_group, @group) @group = nil diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 0cbf3eb58a3..00c50f9d0ad 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -14,6 +14,7 @@ class Groups::GroupMembersController < Groups::ApplicationController @members = @members.search(params[:search]) if params[:search].present? @members = @members.sort(@sort) @members = @members.page(params[:page]).per(50) + @members.includes(:user) @requesters = AccessRequestsFinder.new(@group).execute(current_user) diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 587898a8634..facb25525b5 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -26,7 +26,7 @@ class Groups::LabelsController < Groups::ApplicationController end def create - @label = @group.labels.create(label_params) + @label = Labels::CreateService.new(label_params).execute(group: group) if @label.valid? redirect_to group_labels_path(@group) @@ -40,7 +40,9 @@ class Groups::LabelsController < Groups::ApplicationController end def update - if @label.update_attributes(label_params) + @label = Labels::UpdateService.new(label_params).execute(@label) + + if @label.valid? redirect_back_or_group_labels_path else render :edit diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 256c41e6145..9de0297ecfd 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -1,17 +1,27 @@ class Import::BaseController < ApplicationController private - def find_or_create_namespace(name, owner) - return current_user.namespace if name == owner + def find_or_create_namespace(names, owner) + return current_user.namespace if names == owner return current_user.namespace unless current_user.can_create_group? - begin - name = params[:target_namespace].presence || name - namespace = Group.create!(name: name, path: name, owner: current_user) - namespace.add_owner(current_user) - namespace - rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid - Namespace.find_by_path_or_name(name) + names = params[:target_namespace].presence || names + full_path_namespace = Namespace.find_by_full_path(names) + + return full_path_namespace if full_path_namespace + + names.split('/').inject(nil) do |parent, name| + begin + namespace = Group.create!(name: name, + path: name, + owner: current_user, + parent: parent) + namespace.add_owner(current_user) + + namespace + rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid + Namespace.where(parent: parent).find_by_path_or_name(name) + end end end end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 8e42cdf415f..5ad1e116e4e 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -44,15 +44,15 @@ class Import::BitbucketController < Import::BaseController repo_owner = repo.owner repo_owner = current_user.username if repo_owner == bitbucket_client.user.username - @target_namespace = params[:new_namespace].presence || repo_owner + namespace_path = params[:new_namespace].presence || repo_owner - namespace = find_or_create_namespace(@target_namespace, current_user) + @target_namespace = find_or_create_namespace(namespace_path, current_user) - if current_user.can?(:create_projects, namespace) + if current_user.can?(:create_projects, @target_namespace) # The token in a session can be expired, we need to get most recent one because # Bitbucket::Connection class refreshes it. session[:bitbucket_token] = bitbucket_client.connection.token - @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute + @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, @target_namespace, current_user, credentials).execute else render 'unauthorized' end diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index 69959fe3687..7d1aa8d1ce0 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -1,11 +1,22 @@ class Profiles::AccountsController < Profiles::ApplicationController + include AuthHelper + def show @user = current_user end def unlink provider = params[:provider] - current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml' + identity = current_user.identities.find_by(provider: provider) + + return render_404 unless identity + + if unlink_allowed?(provider) + identity.destroy + else + flash[:alert] = "You are not allowed to unlink your primary login account" + end + redirect_to profile_account_path end end diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index b8b71d295f6..a271e2dfc4b 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController end def user_params - params.require(:user).permit(:notification_email) + params.require(:user).permit(:notification_email, :notified_of_own_activity) end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index e2f81b09adc..f1a93ccb3ad 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -89,4 +89,9 @@ class Projects::ApplicationController < ApplicationController def builds_enabled return render_404 unless @project.feature_available?(:builds, current_user) end + + def update_ref + branch_exists = @repository.find_branch(@target_branch) + @ref = @target_branch if branch_exists + end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 52fc67d162c..80a95c6158b 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -89,11 +89,6 @@ class Projects::BlobController < Projects::ApplicationController private - def update_ref - branch_exists = @repository.find_branch(@target_branch) - @ref = @target_branch if branch_exists - end - def blob @blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path)) diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 886934a3f67..3f3c90a49ab 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,7 +1,7 @@ class Projects::BuildsController < Projects::ApplicationController before_action :build, except: [:index, :cancel_all] before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play] - before_action :authorize_update_build!, except: [:index, :show, :status, :raw] + before_action :authorize_update_build!, except: [:index, :show, :status, :raw, :trace] layout 'project' def index @@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController end def status - render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha) + render json: BuildSerializer + .new(project: @project, user: @current_user) + .represent_status(@build) end def erase diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 1502b734f37..d0c44e297e3 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -31,8 +31,10 @@ class Projects::DeployKeysController < Projects::ApplicationController end def disable - @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy + deploy_key_project = @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]) + return render_404 unless deploy_key_project + deploy_key_project.destroy! redirect_to_repository_settings(@project) end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 278098fcc58..37f6f637ff0 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -57,7 +57,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController def render_ok set_workhorse_internal_api_content_type - render json: Gitlab::Workhorse.git_http_ok(repository, user) + render json: Gitlab::Workhorse.git_http_ok(repository, user, action_name) end def render_http_not_allowed diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index cdb5b4173d3..a50e16fa4ff 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, - :related_branches, :can_create_branch] + :related_branches, :can_create_branch, :rendered_title] # Allow read any issue - before_action :authorize_read_issue!, only: [:show] + before_action :authorize_read_issue!, only: [:show, :rendered_title] # Allow write(create) issue before_action :authorize_create_issue!, only: [:new, :create] @@ -31,7 +31,7 @@ class Projects::IssuesController < Projects::ApplicationController @issuable_meta_data = issuable_meta_data(@issues, @collection_type) if @issues.out_of_range? && @issues.total_pages != 0 - return redirect_to url_for(params.merge(page: @issues.total_pages)) + return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true)) end if params[:label_name].present? @@ -148,7 +148,14 @@ class Projects::IssuesController < Projects::ApplicationController end format.json do - render json: @issue.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + if @issue.valid? + render json: @issue.to_json(methods: [:task_status, :task_status_short], + include: { milestone: {}, + assignee: { only: [:name, :username], methods: [:avatar_url] }, + labels: { methods: :text_color } }) + else + render json: { errors: @issue.errors.full_messages }, status: :unprocessable_entity + end end end @@ -193,6 +200,11 @@ class Projects::IssuesController < Projects::ApplicationController end end + def rendered_title + Gitlab::PollingInterval.set_header(response, interval: 3_000) + render json: { title: view_context.markdown_field(@issue, :title) } + end + protected def issue @@ -253,4 +265,13 @@ class Projects::IssuesController < Projects::ApplicationController :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: [] ) end + + def authenticate_user! + return if current_user + + notice = "Please sign in to create the new issue." + + store_location_for :user, request.fullpath + redirect_to new_user_session_path, notice: notice + end end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 1593b5c1afb..2f55ba4e700 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -29,7 +29,7 @@ class Projects::LabelsController < Projects::ApplicationController end def create - @label = @project.labels.create(label_params) + @label = Labels::CreateService.new(label_params).execute(project: @project) if @label.valid? respond_to do |format| @@ -48,7 +48,9 @@ class Projects::LabelsController < Projects::ApplicationController end def update - if @label.update_attributes(label_params) + @label = Labels::UpdateService.new(label_params).execute(@label) + + if @label.valid? redirect_to namespace_project_labels_path(@project.namespace, @project) else render :edit diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 677a8a1a73a..a79d801991a 100644..100755 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, - :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues + :ci_status, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] @@ -39,10 +39,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController @collection_type = "MergeRequest" @merge_requests = merge_requests_collection @merge_requests = @merge_requests.page(params[:page]) + @merge_requests = @merge_requests.includes(merge_request_diff: :merge_request) @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 - return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) + return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true)) end if params[:label_name].present? @@ -97,31 +98,31 @@ class Projects::MergeRequestsController < Projects::ApplicationController def diffs apply_diff_view_cookie! - @merge_request_diff = - if params[:diff_id] - @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) - else - @merge_request.merge_request_diff - end - - @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff - @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } - - if params[:start_sha].present? - @start_sha = params[:start_sha] - @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } - - unless @start_version - @start_sha = @merge_request_diff.head_commit_sha - @start_version = @merge_request_diff - end - end - - @environment = @merge_request.environments_for(current_user).last - respond_to do |format| format.html { define_discussion_vars } format.json do + @merge_request_diff = + if params[:diff_id] + @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) + else + @merge_request.merge_request_diff + end + + @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff + @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } + + if params[:start_sha].present? + @start_sha = params[:start_sha] + @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } + + unless @start_version + @start_sha = @merge_request_diff.head_commit_sha + @start_version = @merge_request_diff + end + end + + @environment = @merge_request.environments_for(current_user).last + if @start_sha compared_diff_version else @@ -402,7 +403,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController if params[:ref].present? @ref = params[:ref] - @commit = @repository.commit(@ref) + @commit = @repository.commit("refs/heads/#{@ref}") end render layout: false @@ -413,7 +414,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController if params[:ref].present? @ref = params[:ref] - @commit = @target_project.commit(@ref) + @commit = @target_project.commit("refs/heads/#{@ref}") end render layout: false @@ -473,6 +474,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: response end + def pipeline_status + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .represent_status(@merge_request.head_pipeline) + end + def ci_environments_status environments = begin diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index be52b0fa7cf..408c0c60cb0 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -13,14 +13,17 @@ class Projects::MilestonesController < Projects::ApplicationController def index @milestones = case params[:state] - when 'all' then @project.milestones.reorder(due_date: :desc, title: :asc) - when 'closed' then @project.milestones.closed.reorder(due_date: :desc, title: :asc) - else @project.milestones.active.reorder(due_date: :asc, title: :asc) + when 'all' then @project.milestones + when 'closed' then @project.milestones.closed + else @project.milestones.active end - @milestones = @milestones.includes(:project) + @sort = params[:sort] || 'due_date_asc' + @milestones = @milestones.sort(@sort) + respond_to do |format| format.html do + @milestones = @milestones.includes(:project) @milestones = @milestones.page(params[:page]) end format.json do diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 718d9e86bea..43a1abaa662 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController end end + def status + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .represent_status(@pipeline) + end + def stage @stage = pipeline.stage(params[:stage]) return not_found unless @stage diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb index cbfa2afa959..54f9dceddef 100644 --- a/app/controllers/projects/settings/members_controller.rb +++ b/app/controllers/projects/settings/members_controller.rb @@ -9,6 +9,7 @@ module Projects @skip_groups = @group_links.pluck(:group_id) @skip_groups << @project.namespace_id unless @project.personal? + @skip_groups += @project.group.ancestors.pluck(:id) if @project.group @project_members = MembersFinder.new(@project, current_user).execute diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 4f094146348..637b61504d8 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -34,6 +34,7 @@ class Projects::TreeController < Projects::ApplicationController def create_dir return render_404 unless @commit_params.values.all? + update_ref create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.", success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)), failure_path: namespace_project_tree_path(@project.namespace, @project, @ref)) diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index f210f7e61d2..c5e24b9e365 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -124,6 +124,6 @@ class Projects::WikisController < Projects::ApplicationController end def wiki_params - params[:wiki].slice(:title, :content, :format, :message) + params.require(:wiki).permit(:title, :content, :format, :message) end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index b44f38d4a0c..8109427a45f 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,5 +1,4 @@ class RegistrationsController < Devise::RegistrationsController - before_action :signup_enabled? include Recaptcha::Verify def new @@ -21,15 +20,17 @@ class RegistrationsController < Devise::RegistrationsController flash.delete :recaptcha_error render action: 'new' end + rescue Gitlab::Access::AccessDeniedError + redirect_to(new_user_session_path) end def destroy - Users::DestroyService.new(current_user).execute(current_user) + DeleteUserWorker.perform_async(current_user.id, current_user.id) respond_to do |format| format.html do session.try(:destroy) - redirect_to new_user_session_path, notice: "Account successfully removed." + redirect_to new_user_session_path, notice: "Account scheduled for removal." end end end @@ -50,12 +51,6 @@ class RegistrationsController < Devise::RegistrationsController private - def signup_enabled? - unless current_application_settings.signup_enabled? - redirect_to(new_user_session_path) - end - end - def sign_up_params params.require(:user).permit(:username, :email, :email_confirmation, :name, :password) end @@ -65,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController end def resource - @resource ||= User.new(sign_up_params) + @resource ||= Users::CreateService.new(current_user, sign_up_params).build end def devise_mapping diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 612d69cf557..4a579601785 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -6,45 +6,19 @@ class SearchController < ApplicationController layout 'search' def show - if params[:project_id].present? - @project = Project.find_by(id: params[:project_id]) - @project = nil unless can?(current_user, :download_code, @project) - end + search_service = SearchService.new(current_user, params) - if params[:group_id].present? - @group = Group.find_by(id: params[:group_id]) - @group = nil unless can?(current_user, :read_group, @group) - end + @project = search_service.project + @group = search_service.group return if params[:search].blank? @search_term = params[:search] - @scope = params[:scope] - @show_snippets = params[:snippets].eql? 'true' - - @search_results = - if @project - unless %w(blobs notes issues merge_requests milestones wiki_blobs - commits).include?(@scope) - @scope = 'blobs' - end - - Search::ProjectService.new(@project, current_user, params).execute - elsif @show_snippets - unless %w(snippet_blobs snippet_titles).include?(@scope) - @scope = 'snippet_blobs' - end - - Search::SnippetService.new(current_user, params).execute - else - unless %w(projects issues merge_requests milestones).include?(@scope) - @scope = 'projects' - end - Search::GlobalService.new(current_user, params).execute - end - - @search_objects = @search_results.objects(@scope, params[:page]) + @scope = search_service.scope + @show_snippets = search_service.show_snippets? + @search_results = search_service.search_results + @search_objects = search_service.search_objects check_single_commit_result end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 7d81c96262f..d8561871098 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -79,7 +79,7 @@ class SessionsController < Devise::SessionsController if request.referer.present? && (params['redirect_to_referer'] == 'yes') referer_uri = URI(request.referer) if referer_uri.host == Gitlab.config.gitlab.host - referer_uri.path + referer_uri.request_uri else request.fullpath end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 6e29f1e8a65..2683614d2e8 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -39,7 +39,7 @@ class UsersController < ApplicationController format.html { render 'show' } format.json do render json: { - html: view_to_html_string("shared/projects/_list", projects: @projects, remote: true) + html: view_to_html_string("shared/projects/_list", projects: @projects) } end end @@ -65,7 +65,7 @@ class UsersController < ApplicationController format.html { render 'show' } format.json do render json: { - html: view_to_html_string("snippets/_snippets", collection: @snippets, remote: true) + html: view_to_html_string("snippets/_snippets", collection: @snippets) } end end diff --git a/app/finders/group_finder.rb b/app/finders/group_finder.rb new file mode 100644 index 00000000000..24c84d2d1aa --- /dev/null +++ b/app/finders/group_finder.rb @@ -0,0 +1,17 @@ +class GroupFinder + include Gitlab::Allowable + + def initialize(current_user) + @current_user = current_user + end + + def execute(*params) + group = Group.find_by(*params) + + if can?(@current_user, :read_group, group) + group + else + nil + end + end +end diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 08713272947..76715e5970d 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -9,7 +9,7 @@ # state: 'open' or 'closed' or 'all' # group_id: integer # project_id: integer -# milestone_id: integer +# milestone_title: string # assignee_id: integer # search: string # label_name: string diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index fa0e2a5e3d8..e52083f86e4 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder if project? if project - label_ids << project.group.labels if project.group.present? - label_ids << project.labels + if project.group.present? + labels_table = Label.arel_table + + label_ids << Label.where( + labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or( + labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id)) + ) + ) + else + label_ids << project.labels + end end else label_ids << Label.where(group_id: projects.group_ids) diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 1eec45d9cb5..42f0ebd774c 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -9,7 +9,7 @@ # state: 'open' or 'closed' or 'all' # group_id: integer # project_id: integer -# milestone_id: integer +# milestone_title: string # assignee_id: integer # search: string # label_name: string diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 13d33a1c31b..b7f091f334d 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -24,7 +24,6 @@ class TodosFinder def execute items = current_user.todos - items = include_associations(items) items = by_action_id(items) items = by_action(items) items = by_author(items) @@ -39,17 +38,6 @@ class TodosFinder private - def include_associations(items) - return items unless params[:include_associations] - - items.includes( - [ - target: { project: [:route, namespace: :route] }, - author: { namespace: :route }, - ] - ) - end - def action_id? action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a3213581498..e5b811f3300 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -306,4 +306,8 @@ module ApplicationHelper def active_when(condition) 'active' if condition end + + def show_user_callout? + cookies[:user_callout_dismissed] == 'true' + end end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 1ee6c1d3afa..101fe579da2 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -76,5 +76,9 @@ module AuthHelper (current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current end + def unlink_allowed?(provider) + %w(saml cas3).exclude?(provider.to_s) + end + extend self end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index a777db2826b..ec57fec4f99 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -251,6 +251,21 @@ module IssuablesHelper end def selected_template(issuable) - params[:issuable_template] if issuable_templates(issuable).include?(params[:issuable_template]) + params[:issuable_template] if issuable_templates(issuable).any?{ |template| template[:name] == params[:issuable_template] } + end + + def issuable_todo_button_data(issuable, todo, is_collapsed) + { + todo_text: "Add todo", + mark_text: "Mark done", + todo_icon: (is_collapsed ? icon('plus-square') : nil), + mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil), + issuable_id: issuable.id, + issuable_type: issuable.class.name.underscore, + url: namespace_project_todos_path(@project.namespace, @project), + delete_path: (dashboard_todo_path(todo) if todo), + placement: (is_collapsed ? 'left' : nil), + container: (is_collapsed ? 'body' : nil) + } end end diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 5053b937c02..c9e70faa52e 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -19,8 +19,8 @@ module MilestonesHelper end end - def milestones_browse_issuables_path(milestone, type:) - opts = { milestone_title: milestone.title } + def milestones_browse_issuables_path(milestone, state: nil, type:) + opts = { milestone_title: milestone.title, state: state } if @project polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts) @@ -89,10 +89,12 @@ module MilestonesHelper content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" } content.slice!("about ") content << " remaining" + content.html_safe elsif milestone.start_date && milestone.start_date.past? days = milestone.elapsed_days content = content_tag(:strong, days) content << " #{'day'.pluralize(days)} elapsed" + content.html_safe end end diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 2e3a15bc1b9..7f656b8caae 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -6,7 +6,13 @@ module NamespacesHelper def namespaces_options(selected = :current_user, display_path: false, extra_group: nil) groups = current_user.owned_groups + current_user.masters_groups - groups << extra_group if extra_group && !Group.exists?(name: extra_group.name) + unless extra_group.nil? || extra_group.is_a?(Group) + extra_group = Group.find(extra_group) if Namespace.find(extra_group).kind == 'group' + end + + if extra_group && extra_group.is_a?(Group) && (!Group.exists?(name: extra_group.name) || Ability.allowed?(current_user, :read_group, extra_group)) + groups |= [extra_group] + end users = [current_user.namespace] diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index a8f167cbff2..17bfd07e00f 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -6,7 +6,8 @@ module NavHelper current_path?('merge_requests#builds') || current_path?('merge_requests#conflicts') || current_path?('merge_requests#pipelines') || - current_path?('issues#show') + current_path?('issues#show') || + current_path?('milestones#show') if cookies[:collapsed_gutter] == 'true' "page-gutter right-sidebar-collapsed" else @@ -31,7 +32,11 @@ module NavHelper end def layout_nav_class - "page-with-layout-nav" if defined?(nav) && nav + class_name = '' + class_name << " page-with-layout-nav" if defined?(nav) && nav + class_name << " page-with-sub-nav" if content_for?(:sub_nav) + + class_name end def nav_control_class diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index bd0c2cd661e..6b9e4267281 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -407,7 +407,10 @@ module ProjectsHelper def sanitize_repo_path(project, message) return '' unless message.present? - message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") + exports_path = File.join(Settings.shared['path'], 'tmp/project_exports') + filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]") + + filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") end def project_feature_options diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb index b5017080cfb..55f4da0ef85 100644 --- a/app/helpers/sidekiq_helper.rb +++ b/app/helpers/sidekiq_helper.rb @@ -3,9 +3,9 @@ module SidekiqHelper (?<pid>\d+)\s+ (?<cpu>[\d\.,]+)\s+ (?<mem>[\d\.,]+)\s+ - (?<state>[DRSTWXZNLsl\+<]+)\s+ - (?<start>.+)\s+ - (?<command>sidekiq.*\]) + (?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+ + (?<start>.+?)\s+ + (?<command>(?:ruby\d+:\s+)?sidekiq.*\].*) \z/x def parse_sidekiq_ps(line) diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 959ee310867..5c89cbea3fc 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -2,6 +2,7 @@ module SortingHelper def sort_options_hash { sort_value_name => sort_title_name, + sort_value_name_desc => sort_title_name_desc, sort_value_recently_updated => sort_title_recently_updated, sort_value_oldest_updated => sort_title_oldest_updated, sort_value_recently_created => sort_title_recently_created, @@ -50,6 +51,17 @@ module SortingHelper } end + def milestone_sort_options_hash + { + sort_value_name => sort_title_name_asc, + sort_value_name_desc => sort_title_name_desc, + sort_value_due_date_soon => sort_title_due_date_soon, + sort_value_due_date_later => sort_title_due_date_later, + sort_value_start_date_soon => sort_title_start_date_soon, + sort_value_start_date_later => sort_title_start_date_later, + } + end + def sort_title_priority 'Priority' end @@ -90,6 +102,14 @@ module SortingHelper 'Due later' end + def sort_title_start_date_soon + 'Start soon' + end + + def sort_title_start_date_later + 'Start later' + end + def sort_title_name 'Name' end @@ -202,6 +222,14 @@ module SortingHelper 'due_date_desc' end + def sort_value_start_date_soon + 'start_date_asc' + end + + def sort_value_start_date_later + 'start_date_desc' + end + def sort_value_name 'name_asc' end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 847a8fdfca6..4f5adf623f2 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -39,13 +39,9 @@ module TodosHelper namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, todo.target, anchor: anchor) else - if todo.build_failed? - # associated namespace and route would be loaded from the db again if todo.project was used - project = todo.target.project - path = [:pipelines, project.namespace.becomes(Namespace), project, todo.target] - else - path = [todo.target] - end + path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] + + path.unshift(:pipelines) if todo.build_failed? polymorphic_path(path, anchor: anchor) end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 00000000000..9c623c9ba7c --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,7 @@ +module UsersHelper + def user_link(user) + link_to(user.name, user_path(user), + title: user.email, + class: 'has-tooltip commit-committer-link') + end +end diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 79c3c2e62c5..a9b6b33eb5c 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -5,8 +5,8 @@ class BaseMailer < ActionMailer::Base attr_accessor :current_user helper_method :current_user, :can? - default from: Proc.new { default_sender_address.format } - default reply_to: Proc.new { default_reply_to_address.format } + default from: proc { default_sender_address.format } + default reply_to: proc { default_reply_to_address.format } def can? Ability.allowed?(current_user, action, subject) diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 671a0fe98cc..2961e16f5e0 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -131,6 +131,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates :polling_interval_multiplier, + presence: true, + numericality: { greater_than_or_equal_to: 0 } + validates_each :restricted_visibility_levels do |record, attr, value| value&.each do |level| unless Gitlab::VisibilityLevel.options.has_value?(level) @@ -233,7 +237,8 @@ class ApplicationSetting < ActiveRecord::Base signup_enabled: Settings.gitlab['signup_enabled'], terminal_max_session_time: 0, two_factor_grace_period: 48, - user_default_external: false + user_default_external: false, + polling_interval_multiplier: 1 } end diff --git a/app/models/blob.rb b/app/models/blob.rb index 1376b86fdad..f82126f8e65 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -46,6 +46,18 @@ class Blob < SimpleDelegator text? && language && language.name == 'SVG' end + def pdf? + name && File.extname(name) == '.pdf' + end + + def ipython_notebook? + text? && language&.name == 'Jupyter Notebook' + end + + def sketch? + binary? && extname.downcase.delete('.') == 'sketch' + end + def size_within_svg_limits? size <= MAXIMUM_SVG_SIZE end @@ -63,6 +75,12 @@ class Blob < SimpleDelegator end elsif image? || svg? 'image' + elsif pdf? + 'pdf' + elsif ipython_notebook? + 'notebook' + elsif sketch? + 'sketch' elsif text? 'text' else diff --git a/app/models/board.rb b/app/models/board.rb index 2780acc67c0..cf8317891b5 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -5,7 +5,7 @@ class Board < ActiveRecord::Base validates :project, presence: true - def done_list - lists.merge(List.done).take + def closed_list + lists.merge(List.closed).take end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ad0be70c32a..8431c5f228c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -540,6 +540,8 @@ module Ci end def dependencies + return [] if empty_dependencies? + depended_jobs = depends_on_builds return depended_jobs unless options[:dependencies].present? @@ -549,6 +551,10 @@ module Ci end end + def empty_dependencies? + options[:dependencies]&.empty? + end + private def update_artifacts_size diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f12be98c80c..49dec770096 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -164,11 +164,6 @@ module Ci builds.latest.with_artifacts_not_expired.includes(project: [:namespace]) end - # For now the only user who participates is the user who triggered - def participants(_current_user = nil) - Array(user) - end - def valid_commit_sha if self.sha == Gitlab::Git::BLANK_SHA self.errors.add(:sha, " cant be 00000000 (branch removal)") @@ -210,7 +205,7 @@ module Ci end def stuck? - builds.pending.any?(&:stuck?) + builds.pending.includes(:project).any?(&:stuck?) end def retryable? diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 8c71267da65..17b322b5ae3 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -105,6 +105,10 @@ class CommitStatus < ActiveRecord::Base end end + def locking_enabled? + status_changed? + end + def before_sha pipeline.before_sha || Gitlab::Git::BLANK_SHA end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 5101cc7e687..0a1a65da05a 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -31,6 +31,7 @@ module HasStatus WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})>0 THEN 'running' WHEN (#{manual})>0 THEN 'manual' + WHEN (#{created})>0 THEN 'running' ELSE 'failed' END)" end diff --git a/app/models/concerns/importable.rb b/app/models/concerns/importable.rb index 019ef755849..c9331eaf4cc 100644 --- a/app/models/concerns/importable.rb +++ b/app/models/concerns/importable.rb @@ -3,4 +3,7 @@ module Importable attr_accessor :importing alias_method :importing?, :importing + + attr_accessor :imported + alias_method :imported?, :imported end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index e7bd20b322a..b4dded7e27e 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -14,6 +14,7 @@ module Issuable include Awardable include Taskable include TimeTrackable + include Importable # This object is used to gather issuable meta data for displaying # upvotes, downvotes, notes and closing merge requests count for issues and merge requests @@ -50,6 +51,7 @@ module Issuable :email, :public_email, to: :author, + allow_nil: true, prefix: true delegate :name, @@ -98,7 +100,7 @@ module Issuable acts_as_paranoid after_save :update_assignee_cache_counts, if: :assignee_id_changed? - after_save :record_metrics + after_save :record_metrics, unless: :imported? def update_assignee_cache_counts # make sure we flush the cache for both the old *and* new assignees(if they exist) diff --git a/app/models/concerns/repository_mirroring.rb b/app/models/concerns/repository_mirroring.rb new file mode 100644 index 00000000000..fed336c29d6 --- /dev/null +++ b/app/models/concerns/repository_mirroring.rb @@ -0,0 +1,17 @@ +module RepositoryMirroring + def set_remote_as_mirror(name) + config = raw_repository.rugged.config + + # This is used to define repository as equivalent as "git clone --mirror" + config["remote.#{name}.fetch"] = 'refs/*:refs/*' + config["remote.#{name}.mirror"] = true + config["remote.#{name}.prune"] = true + end + + def fetch_mirror(remote, url) + add_remote(remote, url) + set_remote_as_mirror(remote) + fetch_remote(remote, forced: true) + remove_remote(remote) + end +end diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 107e6764ba2..647a6cad3d7 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -41,7 +41,7 @@ module Spammable def check_for_spam error_msg = if Gitlab::Recaptcha.enabled? "Your #{spammable_entity_type} has been recognized as spam. "\ - "You can still submit it by solving Captcha." + "Please, change the content or solve the reCAPTCHA to proceed." else "Your #{spammable_entity_type} has been recognized as spam and has been discarded." end diff --git a/app/models/group.rb b/app/models/group.rb index bd0ecae3da4..60274386103 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -207,7 +207,7 @@ class Group < Namespace end def members_with_parents - GroupMember.non_request.where(source_id: ancestors.map(&:id).push(id)) + GroupMember.non_request.where(source_id: ancestors.pluck(:id).push(id)) end def users_with_parents diff --git a/app/models/issue.rb b/app/models/issue.rb index 602eed86d9e..f9704b0d754 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -40,6 +40,8 @@ class Issue < ActiveRecord::Base scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) } + after_save :expire_etag_cache + attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true @@ -59,10 +61,6 @@ class Issue < ActiveRecord::Base before_transition any => :closed do |issue| issue.closed_at = Time.zone.now end - - before_transition closed: any do |issue| - issue.closed_at = nil - end end def hook_attrs @@ -211,9 +209,8 @@ class Issue < ActiveRecord::Base due_date.try(:past?) || false end - # Only issues on public projects should be checked for spam def check_for_spam? - project.public? + project.public? && (title_changed? || description_changed?) end def as_json(options = {}) @@ -257,4 +254,13 @@ class Issue < ActiveRecord::Base def publicly_visible? project.public? && !confidential? end + + def expire_etag_cache + key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path( + project.namespace, + project, + self + ) + Gitlab::EtagCaching::Store.new.touch(key) + end end diff --git a/app/models/list.rb b/app/models/list.rb index 1e5da7f4dd4..fbd19acd1f5 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -2,7 +2,7 @@ class List < ActiveRecord::Base belongs_to :board belongs_to :label - enum list_type: { label: 1, done: 2 } + enum list_type: { label: 1, closed: 2 } validates :board, :list_type, presence: true validates :label, :position, presence: true, if: :label? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 38eefad96b6..b2725a314ad 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -3,11 +3,9 @@ class MergeRequest < ActiveRecord::Base include Issuable include Referable include Sortable - include Importable belongs_to :target_project, class_name: "Project" belongs_to :source_project, class_name: "Project" - belongs_to :project, foreign_key: :target_project_id belongs_to :merge_user, class_name: "User" has_many :merge_request_diffs, dependent: :destroy @@ -155,8 +153,10 @@ class MergeRequest < ActiveRecord::Base # # Returns an ActiveRecord::Relation. def self.in_projects(relation) - source = where(source_project_id: relation).select(:id) - target = where(target_project_id: relation).select(:id) + # unscoping unnecessary conditions that'll be applied + # when executing `where("merge_requests.id IN (#{union.to_sql})")` + source = unscoped.where(source_project_id: relation).select(:id) + target = unscoped.where(target_project_id: relation).select(:id) union = Gitlab::SQL::Union.new([source, target]) where("merge_requests.id IN (#{union.to_sql})") @@ -541,6 +541,10 @@ class MergeRequest < ActiveRecord::Base target_project != source_project end + def project + target_project + end + # If the merge request closes any issues, save this information in the # `MergeRequestsClosingIssues` model. This is a performance optimization. # Calculating this information for a number of merge requests requires diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index baee00b8fcd..6ad56b842b2 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -177,6 +177,16 @@ class MergeRequestDiff < ActiveRecord::Base st_commits.count end + def utf8_st_diffs + return [] if st_diffs.blank? + + st_diffs.map do |diff| + diff.each do |k, v| + diff[k] = encode_utf8(v) if v.respond_to?(:encoding) + end + end + end + private # Old GitLab implementations may have generated diffs as ["--broken-diff"]. @@ -270,14 +280,6 @@ class MergeRequestDiff < ActiveRecord::Base project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha) end - def utf8_st_diffs - st_diffs.map do |diff| - diff.each do |k, v| - diff[k] = encode_utf8(v) if v.respond_to?(:encoding) - end - end - end - # # #save or #update_attributes providing changes on serialized attributes do a lot of # serialization and deserialization calls resulting in bad performance. diff --git a/app/models/milestone.rb b/app/models/milestone.rb index c0deb59ec4c..ac205b9b738 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -30,7 +30,7 @@ class Milestone < ActiveRecord::Base validates :title, presence: true, uniqueness: { scope: :project_id } validates :project, presence: true - validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? } + validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? } strip_attributes :title @@ -107,6 +107,21 @@ class Milestone < ActiveRecord::Base end end + def self.sort(method) + case method.to_s + when 'due_date_asc' + reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) + when 'due_date_desc' + reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC')) + when 'start_date_asc' + reorder(Gitlab::Database.nulls_last_order('start_date', 'ASC')) + when 'start_date_desc' + reorder(Gitlab::Database.nulls_last_order('start_date', 'DESC')) + else + order_by(method) + end + end + ## # Returns the String necessary to reference this Milestone in Markdown # diff --git a/app/models/namespace.rb b/app/models/namespace.rb index d350f1d6770..9bfa731785f 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -120,10 +120,10 @@ class Namespace < ActiveRecord::Base # Move the namespace directory in all storages paths used by member projects repository_storage_paths.each do |repository_storage_path| # Ensure old directory exists before moving it - gitlab_shell.add_namespace(repository_storage_path, path_was) + gitlab_shell.add_namespace(repository_storage_path, full_path_was) - unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) - Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" + unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}" # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs @@ -131,8 +131,8 @@ class Namespace < ActiveRecord::Base end end - Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) - Gitlab::PagesTransfer.new.rename_namespace(path_was, path) + Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path) + Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path) remove_exports! @@ -150,12 +150,12 @@ class Namespace < ActiveRecord::Base end def any_project_has_container_registry_tags? - projects.any?(&:has_container_registry_tags?) + all_projects.any?(&:has_container_registry_tags?) end def send_update_instructions projects.each do |project| - project.send_move_instructions("#{path_was}/#{project.path}") + project.send_move_instructions("#{full_path_was}/#{project.path}") end end @@ -195,7 +195,7 @@ class Namespace < ActiveRecord::Base # Scopes the model on direct and indirect children of the record def descendants - self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC') + self.class.joins(:route).merge(Route.inside_path(route.path)).reorder('routes.path ASC') end def user_ids_for_project_authorizations @@ -214,6 +214,12 @@ class Namespace < ActiveRecord::Base @old_repository_storage_paths ||= repository_storage_paths end + # Includes projects from this namespace and projects from all subgroups + # that belongs to this namespace + def all_projects + Project.inside_path(full_path) + end + private def repository_storage_paths @@ -221,7 +227,7 @@ class Namespace < ActiveRecord::Base # pending delete. Unscoping also get rids of the default order, which causes # problems with SELECT DISTINCT. Project.unscoped do - projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path) + all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path) end end @@ -230,10 +236,10 @@ class Namespace < ActiveRecord::Base old_repository_storage_paths.each do |repository_storage_path| # Move namespace directory into trash. # We will remove it later async - new_path = "#{path}+#{id}+deleted" + new_path = "#{full_path}+#{id}+deleted" - if gitlab_shell.mv_namespace(repository_storage_path, path, new_path) - message = "Namespace directory \"#{path}\" moved to \"#{new_path}\"" + if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path) + message = "Namespace directory \"#{full_path}\" moved to \"#{new_path}\"" Gitlab::AppLogger.info message # Remove namespace directroy async with delay so diff --git a/app/models/note.rb b/app/models/note.rb index e22e96aec6f..16d66cb1427 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -37,6 +37,7 @@ class Note < ActiveRecord::Base has_many :todos, dependent: :destroy has_many :events, as: :target, dependent: :destroy + has_one :system_note_metadata delegate :gfm_reference, :local_reference, to: :noteable delegate :name, to: :project, prefix: true @@ -70,7 +71,9 @@ class Note < ActiveRecord::Base scope :fresh, ->{ order(created_at: :asc, id: :asc) } scope :inc_author_project, ->{ includes(:project, :author) } scope :inc_author, ->{ includes(:author) } - scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) } + scope :inc_relations_for_view, -> do + includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata) + end scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) } scope :non_diff_notes, ->{ where(type: ['Note', nil]) } diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 52577bd52ea..e4726e62e93 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base def set_events return if custom? - EMAIL_EVENTS.each do |event| - events[event] = false - end + self.events = {} end # Validates store accessors values as boolean # It is a text field so it does not cast correct boolean values in JSON def events_to_boolean EMAIL_EVENTS.each do |event| - events[event] = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(events[event]) + bool = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(public_send(event)) + + events[event] = bool end end + + # Allow people to receive failed pipeline notifications if they already have + # custom notifications enabled, as these are more like mentions than the other + # custom settings. + def failed_pipeline + bool = super + + bool.nil? || bool + end end diff --git a/app/models/project.rb b/app/models/project.rb index 13c5c181cc5..2469e6f8523 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -114,6 +114,8 @@ class Project < ActiveRecord::Base has_one :kubernetes_service, dependent: :destroy, inverse_of: :project has_one :prometheus_service, dependent: :destroy, inverse_of: :project has_one :mock_ci_service, dependent: :destroy + has_one :mock_deployment_service, dependent: :destroy + has_one :mock_monitoring_service, dependent: :destroy has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link @@ -239,7 +241,7 @@ class Project < ActiveRecord::Base # We need routes alias rs for JOIN so it does not conflict with # includes(:route) which we use in ProjectsFinder. joins("INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project'"). - where('rs.path LIKE ?', "#{path}/%") + where('rs.path LIKE ?', "#{sanitize_sql_like(path)}/%") end # "enabled" here means "not disabled". It includes private features! @@ -315,20 +317,15 @@ class Project < ActiveRecord::Base ntable = Namespace.arel_table pattern = "%#{query}%" - projects = select(:id).where( + # unscoping unnecessary conditions that'll be applied + # when executing `where("projects.id IN (#{union.to_sql})")` + projects = unscoped.select(:id).where( ptable[:path].matches(pattern). or(ptable[:name].matches(pattern)). or(ptable[:description].matches(pattern)) ) - # We explicitly remove any eager loading clauses as they're: - # - # 1. Not needed by this query - # 2. Combined with .joins(:namespace) lead to all columns from the - # projects & namespaces tables being selected, leading to a SQL error - # due to the columns of all UNION'd queries no longer being the same. - namespaces = select(:id). - except(:includes). + namespaces = unscoped.select(:id). joins(:namespace). where(ntable[:name].matches(pattern)) @@ -557,6 +554,10 @@ class Project < ActiveRecord::Base import_type == 'gitea' end + def github_import? + import_type == 'github' + end + def check_limit unless creator.can_create_project? || namespace.kind == 'group' projects_limit = creator.projects_limit @@ -874,13 +875,9 @@ class Project < ActiveRecord::Base end def http_url_to_repo(user = nil) - url = web_url - - if user - url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" } - end + credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user) - "#{url}.git" + Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url end def user_can_push_to_empty_repo?(user) diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index 200be99f36b..75834103db5 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -6,7 +6,7 @@ class ChatNotificationService < Service default_value_for :category, 'chat' prop_accessor :webhook, :username, :channel - boolean_accessor :notify_only_broken_pipelines + boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch validates :webhook, presence: true, url: true, if: :activated? @@ -17,6 +17,7 @@ class ChatNotificationService < Service if properties.nil? self.properties = {} self.notify_only_broken_pipelines = true + self.notify_only_default_branch = true end end @@ -29,6 +30,19 @@ class ChatNotificationService < Service pipeline wiki_page] end + def fields + default_fields + build_event_channels + end + + def default_fields + [ + { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" }, + { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, + { type: 'checkbox', name: 'notify_only_broken_pipelines' }, + { type: 'checkbox', name: 'notify_only_default_branch' }, + ] + end + def execute(data) return unless supported_events.include?(data[:object_kind]) return unless webhook.present? @@ -123,6 +137,17 @@ class ChatNotificationService < Service end def should_pipeline_be_notified?(data) + notify_for_ref?(data) && notify_for_pipeline?(data) + end + + def notify_for_ref?(data) + return true if data[:object_attributes][:tag] + return true unless notify_only_default_branch + + data[:object_attributes][:ref] == project.default_branch + end + + def notify_for_pipeline?(data) case data[:object_attributes][:status] when 'success' !notify_only_broken_pipelines? diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index eef403dba92..3b90fd1c2c7 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -62,7 +62,7 @@ class JiraService < IssueTrackerService def help "You need to configure JIRA before enabling this service. For more details read the - [JIRA service documentation](#{help_page_url('project_services/jira')})." + [JIRA service documentation](#{help_page_url('user/project/integrations/jira')})." end def title diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 02fbd5497fa..9c56518c991 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -22,22 +22,21 @@ class KubernetesService < DeploymentService with_options presence: true, if: :activated? do validates :api_url, url: true validates :token - - validates :namespace, - format: { - with: Gitlab::Regex.kubernetes_namespace_regex, - message: Gitlab::Regex.kubernetes_namespace_regex_message, - }, - length: 1..63 end + validates :namespace, + allow_blank: true, + length: 1..63, + if: :activated?, + format: { + with: Gitlab::Regex.kubernetes_namespace_regex, + message: Gitlab::Regex.kubernetes_namespace_regex_message + } + after_save :clear_reactive_cache! def initialize_properties - if properties.nil? - self.properties = {} - self.namespace = "#{project.path}-#{project.id}" if project.present? - end + self.properties = {} if properties.nil? end def title @@ -62,7 +61,7 @@ class KubernetesService < DeploymentService { type: 'text', name: 'namespace', title: 'Kubernetes namespace', - placeholder: 'Kubernetes namespace' }, + placeholder: namespace_placeholder }, { type: 'text', name: 'api_url', title: 'API URL', @@ -92,7 +91,7 @@ class KubernetesService < DeploymentService variables = [ { key: 'KUBE_URL', value: api_url, public: true }, { key: 'KUBE_TOKEN', value: token, public: false }, - { key: 'KUBE_NAMESPACE', value: namespace, public: true } + { key: 'KUBE_NAMESPACE', value: namespace_variable, public: true } ] if ca_pem.present? @@ -135,8 +134,26 @@ class KubernetesService < DeploymentService { pods: pods } end + TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze + private + def namespace_placeholder + default_namespace || TEMPLATE_PLACEHOLDER + end + + def namespace_variable + if namespace.present? + namespace + else + default_namespace + end + end + + def default_namespace + "#{project.path}-#{project.id}" if project.present? + end + def build_kubeclient!(api_path: 'api', api_version: 'v1') raise "Incomplete settings" unless api_url && namespace && token diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index 1156d050622..0362ed172c7 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -22,19 +22,11 @@ class MattermostService < ChatNotificationService </ol>' end - def fields - default_fields + build_event_channels - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/โฆ' }, - { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - end - def default_channel_placeholder "Channel handle (e.g. town-square)" end + + def webhook_placeholder + 'http://mattermost.example.com/hooks/โฆ' + end end diff --git a/app/models/project_services/mock_deployment_service.rb b/app/models/project_services/mock_deployment_service.rb new file mode 100644 index 00000000000..59a3811ce5d --- /dev/null +++ b/app/models/project_services/mock_deployment_service.rb @@ -0,0 +1,18 @@ +class MockDeploymentService < DeploymentService + def title + 'Mock deployment' + end + + def description + 'Mock deployment service' + end + + def self.to_param + 'mock_deployment' + end + + # No terminals support + def terminals(environment) + [] + end +end diff --git a/app/models/project_services/mock_monitoring_service.rb b/app/models/project_services/mock_monitoring_service.rb new file mode 100644 index 00000000000..dd04e04e198 --- /dev/null +++ b/app/models/project_services/mock_monitoring_service.rb @@ -0,0 +1,17 @@ +class MockMonitoringService < MonitoringService + def title + 'Mock monitoring' + end + + def description + 'Mock monitoring service' + end + + def self.to_param + 'mock_monitoring' + end + + def metrics(environment) + JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json')) + end +end diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 375966b9efc..6854d2243d7 100644 --- a/app/models/project_services/prometheus_service.rb +++ b/app/models/project_services/prometheus_service.rb @@ -30,7 +30,14 @@ class PrometheusService < MonitoringService end def help - 'Retrieves `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.' + <<-MD.strip_heredoc + Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total` + and `container_memory_usage_bytes` from the configured Prometheus server. + + If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html) + or have set up your own Prometheus server, an `environment` label is required on each metric to + [identify the Environment](https://docs.gitlab.com/ce/user/project/integrations/prometheus.html#metrics-and-labels). + MD end def self.to_param @@ -67,16 +74,16 @@ class PrometheusService < MonitoringService def calculate_reactive_cache(environment_slug) return unless active? && project && !project.pending_delete? - memory_query = %{sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})/1024/1024} - cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))} + memory_query = %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024} + cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100} { success: true, metrics: { - # Memory used in MB + # Average Memory used in MB memory_values: client.query_range(memory_query, start: 8.hours.ago), memory_current: client.query(memory_query), - # CPU Usage rate in cores. + # Average CPU Utilization cpu_values: client.query_range(cpu_query, start: 8.hours.ago), cpu_current: client.query(cpu_query) }, diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index b657db6f9ee..71da0af75f6 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -21,19 +21,11 @@ class SlackService < ChatNotificationService </ol>' end - def fields - default_fields + build_event_channels - end - - def default_fields - [ - { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/โฆ' }, - { type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, - { type: 'checkbox', name: 'notify_only_broken_pipelines' }, - ] - end - def default_channel_placeholder "Channel name (e.g. general)" end + + def webhook_placeholder + 'https://hooks.slack.com/services/โฆ' + end end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 8a53e974b6f..6d6644053f8 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -169,6 +169,9 @@ class ProjectTeam # Lookup only the IDs we need user_ids = user_ids - access.keys + + return access if user_ids.empty? + users_access = project.project_authorizations. where(user: user_ids). group(:user_id). diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index 539b31780b3..70eef359cdd 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -42,8 +42,11 @@ class ProjectWiki url_to_repo end - def http_url_to_repo - [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') + def http_url_to_repo(user = nil) + url = "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git" + credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user) + + Gitlab::UrlSanitizer.new(url, credentials: credentials).full_url end def wiki_base_path diff --git a/app/models/repository.rb b/app/models/repository.rb index 6ab04440ca8..dc1c1fab880 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -2,6 +2,7 @@ require 'securerandom' class Repository include Gitlab::ShellAdapter + include RepositoryMirroring attr_accessor :path_with_namespace, :project @@ -58,13 +59,13 @@ class Repository def raw_repository return nil unless path_with_namespace - @raw_repository ||= Gitlab::Git::Repository.new(path_to_repo) + @raw_repository ||= initialize_raw_repository end # Return absolute path to repository def path_to_repo @path_to_repo ||= File.expand_path( - File.join(@project.repository_storage_path, path_with_namespace + ".git") + File.join(repository_storage_path, path_with_namespace + ".git") ) end @@ -145,12 +146,7 @@ class Repository # may cause the branch to "disappear" erroneously or have the wrong SHA. # # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392 - raw_repo = - if fresh_repo - Gitlab::Git::Repository.new(path_to_repo) - else - raw_repository - end + raw_repo = fresh_repo ? initialize_raw_repository : raw_repository raw_repo.find_branch(name) end @@ -401,10 +397,6 @@ class Repository expire_tags_cache end - def before_import - expire_content_cache - end - # Runs code after the HEAD of a repository is changed. def after_change_head expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys) @@ -508,9 +500,7 @@ class Repository end end - def branch_names - branches.map(&:name) - end + delegate :branch_names, to: :raw_repository cache_method :branch_names, fallback: [] delegate :tag_names, to: :raw_repository @@ -981,7 +971,13 @@ class Repository end def is_ancestor?(ancestor_id, descendant_id) - merge_base(ancestor_id, descendant_id) == ancestor_id + Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| + if is_enabled + raw_repository.is_ancestor?(ancestor_id, descendant_id) + else + merge_base_commit(ancestor_id, descendant_id) == ancestor_id + end + end end def empty_repo? @@ -1027,6 +1023,23 @@ class Repository rugged.references.delete(tmp_ref) if tmp_ref end + def add_remote(name, url) + raw_repository.remote_add(name, url) + rescue Rugged::ConfigError + raw_repository.remote_update(name, url: url) + end + + def remove_remote(name) + raw_repository.remote_delete(name) + true + rescue Rugged::ConfigError + false + end + + def fetch_remote(remote, forced: false, no_tags: false) + gitlab_shell.fetch_remote(repository_storage_path, path_with_namespace, remote, forced: forced, no_tags: no_tags) + end + def fetch_ref(source_path, source_ref, target_ref) args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) @@ -1144,4 +1157,12 @@ class Repository def repository_event(event, tags = {}) Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) end + + def repository_storage_path + @project.repository_storage_path + end + + def initialize_raw_repository + Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git') + end end diff --git a/app/models/route.rb b/app/models/route.rb index 41e6eb7cb73..4b3efab5c3c 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -10,9 +10,11 @@ class Route < ActiveRecord::Base after_update :rename_descendants + scope :inside_path, -> (path) { where('routes.path LIKE ?', "#{sanitize_sql_like(path)}/%") } + def rename_descendants if path_changed? || name_changed? - descendants = Route.where('path LIKE ?', "#{path_was}/%") + descendants = self.class.inside_path(path_was) descendants.each do |route| attributes = {} diff --git a/app/models/service.rb b/app/models/service.rb index e73f7e5d1a3..5a0ec58d193 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -25,7 +25,7 @@ class Service < ActiveRecord::Base belongs_to :project, inverse_of: :services has_one :service_hook - validates :project_id, presence: true, unless: Proc.new { |service| service.template? } + validates :project_id, presence: true, unless: proc { |service| service.template? } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :issue_trackers, -> { where(category: 'issue_tracker') } @@ -238,7 +238,9 @@ class Service < ActiveRecord::Base slack teamcity ] - service_names << 'mock_ci' if Rails.env.development? + if Rails.env.development? + service_names += %w[mock_ci mock_deployment mock_monitoring] + end service_names.sort_by(&:downcase) end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index dbd564e5e7d..30aca62499c 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -132,7 +132,8 @@ class Snippet < ActiveRecord::Base end def check_for_spam? - public? + visibility_level_changed?(to: Snippet::PUBLIC) || + (public? && (title_changed? || content_changed?)) end def spammable_entity_type diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb new file mode 100644 index 00000000000..1e6fc837a75 --- /dev/null +++ b/app/models/system_note_metadata.rb @@ -0,0 +1,11 @@ +class SystemNoteMetadata < ActiveRecord::Base + ICON_TYPES = %w[ + commit merge confidential visible label assignee cross_reference + title time_tracking branch milestone discussion task moved opened closed merged + ].freeze + + validates :note, presence: true + validates :action, inclusion: ICON_TYPES, allow_nil: true + + belongs_to :note +end diff --git a/app/models/user.rb b/app/models/user.rb index 8c7ad5d5174..95a766f2ede 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -22,6 +22,7 @@ class User < ActiveRecord::Base default_value_for :hide_no_ssh_key, false default_value_for :hide_no_password, false default_value_for :project_view, :files + default_value_for :notified_of_own_activity, false attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, @@ -115,7 +116,9 @@ class User < ActiveRecord::Base validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true validates :bio, length: { maximum: 255 }, allow_blank: true - validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 } + validates :projects_limit, + presence: true, + numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE } validates :username, namespace: true, presence: true, @@ -126,10 +129,9 @@ class User < ActiveRecord::Base validate :unique_email, if: ->(user) { user.email_changed? } validate :owns_notification_email, if: ->(user) { user.notification_email_changed? } validate :owns_public_email, if: ->(user) { user.public_email_changed? } + validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } - before_validation :generate_password, on: :create - before_validation :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } before_validation :sanitize_attrs before_validation :set_notification_email, if: ->(user) { user.email_changed? } before_validation :set_public_email, if: ->(user) { user.public_email_changed? } @@ -139,8 +141,6 @@ class User < ActiveRecord::Base before_save :ensure_external_user_rights after_save :ensure_namespace_correct after_initialize :set_projects_limit - before_create :check_confirmation_email - after_create :post_create_hook after_destroy :post_destroy_hook # User's Layout preference @@ -324,6 +324,8 @@ class User < ActiveRecord::Base end def find_by_personal_access_token(token_string) + return unless token_string + PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user end @@ -382,10 +384,8 @@ class User < ActiveRecord::Base "#{self.class.reference_prefix}#{username}" end - def generate_password - if force_random_password - self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min) - end + def skip_confirmation=(bool) + skip_confirmation! if bool end def generate_reset_token @@ -397,10 +397,6 @@ class User < ActiveRecord::Base @reset_token end - def check_confirmation_email - skip_confirmation! unless current_application_settings.send_user_confirmation_email - end - def recently_sent_password_reset? reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago end @@ -639,8 +635,10 @@ class User < ActiveRecord::Base end def fork_of(project) - links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects) - + links = ForkedProjectLink.where( + forked_from_project_id: project, + forked_to_project_id: personal_projects.unscope(:order) + ) if links.any? links.first.forked_to_project else @@ -795,12 +793,6 @@ class User < ActiveRecord::Base end end - def post_create_hook - log_info("User \"#{name}\" (#{email}) was created") - notification_service.new_user(self, @reset_token) if created_by_id - system_hook_service.execute_hooks_for(self, :create) - end - def post_destroy_hook log_info("User \"#{name}\" (#{email}) was removed") system_hook_service.execute_hooks_for(self, :destroy) diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb index 184f5fd4b52..184b4b7a681 100644 --- a/app/serializers/build_action_entity.rb +++ b/app/serializers/build_action_entity.rb @@ -11,4 +11,6 @@ class BuildActionEntity < Grape::Entity build.project, build) end + + expose :playable?, as: :playable end diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb index 5bcbe285052..b804d6d0e8a 100644 --- a/app/serializers/build_entity.rb +++ b/app/serializers/build_entity.rb @@ -16,12 +16,20 @@ class BuildEntity < Grape::Entity path_to(:play_namespace_project_build, build) end + expose :playable?, as: :playable expose :created_at expose :updated_at + expose :detailed_status, as: :status, with: StatusEntity private + alias_method :build, :object + def path_to(route, build) send("#{route}_path", build.project.namespace, build.project, build) end + + def detailed_status + build.detailed_status(request.user) + end end diff --git a/app/serializers/build_serializer.rb b/app/serializers/build_serializer.rb new file mode 100644 index 00000000000..79b67001199 --- /dev/null +++ b/app/serializers/build_serializer.rb @@ -0,0 +1,8 @@ +class BuildSerializer < BaseSerializer + entity BuildEntity + + def represent_status(resource) + data = represent(resource, { only: [:status] }) + data.fetch(:status, {}) + end +end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 4c017960628..4ff15a78115 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -9,6 +9,13 @@ class EnvironmentEntity < Grape::Entity expose :last_deployment, using: DeploymentEntity expose :stop_action? + expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment| + metrics_namespace_project_environment_path( + environment.project.namespace, + environment.project, + environment) + end + expose :environment_path do |environment| namespace_project_environment_path( environment.project.namespace, diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 61f0f11d7d2..3f16dd66d54 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity end expose :details do - expose :status do |pipeline, options| - StatusEntity.represent( - pipeline.detailed_status(request.user), - options) - end - + expose :detailed_status, as: :status, with: StatusEntity expose :duration expose :finished_at expose :stages, using: StageEntity @@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity pipeline.cancelable? && can?(request.user, :update_pipeline, pipeline) end + + def detailed_status + pipeline.detailed_status(request.user) + end end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index ab2d3d5a3ec..7829df9fada 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer super(resource, opts) end end + + def represent_status(resource) + return {} unless resource.present? + + data = represent(resource, { only: [{ details: [:status] }] }) + data.dig(:details, :status) || {} + end end diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index 47066bebfb1..dfd9d1584a1 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -1,7 +1,7 @@ class StatusEntity < Grape::Entity include RequestAwareEntity - expose :icon, :text, :label, :group + expose :icon, :favicon, :text, :label, :group expose :has_details?, as: :has_details expose :details_path diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 745c2c4b681..a0cb00dba58 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -24,6 +24,10 @@ class BaseService Gitlab::AppLogger.info message end + def log_error(message) + Gitlab::AppLogger.error message + end + def system_hook_service SystemHooksService.new end diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index f6275a63109..fd9ff115eab 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -12,7 +12,7 @@ module Boards def create_board! board = project.boards.create - board.lists.create(list_type: :done) + board.lists.create(list_type: :closed) board end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 83f51947bd4..533e6787855 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -3,7 +3,7 @@ module Boards class ListService < BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute - issues = without_board_labels(issues) unless movable_list? + issues = without_board_labels(issues) unless list issues = with_list_label(issues) if movable_list? issues.order_by_position_and_priority end @@ -41,7 +41,7 @@ module Boards end def set_state - params[:state] = list && list.done? ? 'closed' : 'opened' + params[:state] = list && list.closed? ? 'closed' : 'opened' end def board_label_ids diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 2a9981ab884..d5735f13c1e 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -48,8 +48,8 @@ module Boards end def issue_state - return 'reopen' if moving_from_list.done? - return 'close' if moving_to_list.done? + return 'reopen' if moving_from_list.closed? + return 'close' if moving_to_list.closed? end def add_label_ids diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 2935d00c075..33edcd60944 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -5,8 +5,6 @@ module Ci def execute(pipeline) @pipeline = pipeline - ensure_created_builds! # TODO, remove me in 9.0 - new_builds = stage_indexes_of_created_builds.map do |index| process_stage(index) @@ -73,18 +71,5 @@ module Ci def created_builds pipeline.builds.created end - - # This method is DEPRECATED and should be removed in 9.0. - # - # We need it to maintain backwards compatibility with previous versions - # when builds were not created within one transaction with the pipeline. - # - def ensure_created_builds! - return if created_builds.any? - - Ci::CreatePipelineBuildsService - .new(project, current_user) - .execute(pipeline) - end end end diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb index 574561adc4c..f72ddbf690c 100644 --- a/app/services/ci/retry_pipeline_service.rb +++ b/app/services/ci/retry_pipeline_service.rb @@ -7,14 +7,14 @@ module Ci raise Gitlab::Access::AccessDeniedError end - pipeline.builds.failed_or_canceled.find_each do |build| + pipeline.builds.latest.failed_or_canceled.find_each do |build| next unless build.retryable? Ci::RetryBuildService.new(project, current_user) .reprocess(build) end - pipeline.builds.skipped.find_each do |skipped| + pipeline.builds.latest.skipped.find_each do |skipped| retry_optimistic_lock(skipped) { |build| build.process } end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index b07338d500a..673ed02f952 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -25,12 +25,12 @@ class CreateBranchService < BaseService private def create_master_branch - project.repository.commit_file( + project.repository.create_file( current_user, '/README.md', '', message: 'Add README.md', - branch_name: 'master', - update: false) + branch_name: 'master' + ) end end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 4e878ec556a..1d65c76d282 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -1,6 +1,8 @@ module Groups class UpdateService < Groups::BaseService def execute + reject_parent_id! + # check that user is allowed to set specified visibility_level new_visibility = params[:visibility_level] if new_visibility && new_visibility.to_i != group.visibility_level @@ -22,5 +24,11 @@ module Groups false end end + + private + + def reject_parent_id! + params.except!(:parent_id) + end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index a444c78b609..b7fe5cb168b 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -19,7 +19,7 @@ module Issues if issue.previous_changes.include?('title') || issue.previous_changes.include?('description') - todo_service.update_issue(issue, current_user) + todo_service.update_issue(issue, current_user, old_mentioned_users) end if issue.previous_changes.include?('milestone_id') diff --git a/app/services/labels/base_service.rb b/app/services/labels/base_service.rb new file mode 100644 index 00000000000..91d72a57b4e --- /dev/null +++ b/app/services/labels/base_service.rb @@ -0,0 +1,161 @@ +module Labels + class BaseService < ::BaseService + COLOR_NAME_TO_HEX = { + black: '#000000', + silver: '#C0C0C0', + gray: '#808080', + white: '#FFFFFF', + maroon: '#800000', + red: '#FF0000', + purple: '#800080', + fuchsia: '#FF00FF', + green: '#008000', + lime: '#00FF00', + olive: '#808000', + yellow: '#FFFF00', + navy: '#000080', + blue: '#0000FF', + teal: '#008080', + aqua: '#00FFFF', + orange: '#FFA500', + aliceblue: '#F0F8FF', + antiquewhite: '#FAEBD7', + aquamarine: '#7FFFD4', + azure: '#F0FFFF', + beige: '#F5F5DC', + bisque: '#FFE4C4', + blanchedalmond: '#FFEBCD', + blueviolet: '#8A2BE2', + brown: '#A52A2A', + burlywood: '#DEB887', + cadetblue: '#5F9EA0', + chartreuse: '#7FFF00', + chocolate: '#D2691E', + coral: '#FF7F50', + cornflowerblue: '#6495ED', + cornsilk: '#FFF8DC', + crimson: '#DC143C', + darkblue: '#00008B', + darkcyan: '#008B8B', + darkgoldenrod: '#B8860B', + darkgray: '#A9A9A9', + darkgreen: '#006400', + darkgrey: '#A9A9A9', + darkkhaki: '#BDB76B', + darkmagenta: '#8B008B', + darkolivegreen: '#556B2F', + darkorange: '#FF8C00', + darkorchid: '#9932CC', + darkred: '#8B0000', + darksalmon: '#E9967A', + darkseagreen: '#8FBC8F', + darkslateblue: '#483D8B', + darkslategray: '#2F4F4F', + darkslategrey: '#2F4F4F', + darkturquoise: '#00CED1', + darkviolet: '#9400D3', + deeppink: '#FF1493', + deepskyblue: '#00BFFF', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1E90FF', + firebrick: '#B22222', + floralwhite: '#FFFAF0', + forestgreen: '#228B22', + gainsboro: '#DCDCDC', + ghostwhite: '#F8F8FF', + gold: '#FFD700', + goldenrod: '#DAA520', + greenyellow: '#ADFF2F', + grey: '#808080', + honeydew: '#F0FFF0', + hotpink: '#FF69B4', + indianred: '#CD5C5C', + indigo: '#4B0082', + ivory: '#FFFFF0', + khaki: '#F0E68C', + lavender: '#E6E6FA', + lavenderblush: '#FFF0F5', + lawngreen: '#7CFC00', + lemonchiffon: '#FFFACD', + lightblue: '#ADD8E6', + lightcoral: '#F08080', + lightcyan: '#E0FFFF', + lightgoldenrodyellow: '#FAFAD2', + lightgray: '#D3D3D3', + lightgreen: '#90EE90', + lightgrey: '#D3D3D3', + lightpink: '#FFB6C1', + lightsalmon: '#FFA07A', + lightseagreen: '#20B2AA', + lightskyblue: '#87CEFA', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#B0C4DE', + lightyellow: '#FFFFE0', + limegreen: '#32CD32', + linen: '#FAF0E6', + mediumaquamarine: '#66CDAA', + mediumblue: '#0000CD', + mediumorchid: '#BA55D3', + mediumpurple: '#9370DB', + mediumseagreen: '#3CB371', + mediumslateblue: '#7B68EE', + mediumspringgreen: '#00FA9A', + mediumturquoise: '#48D1CC', + mediumvioletred: '#C71585', + midnightblue: '#191970', + mintcream: '#F5FFFA', + mistyrose: '#FFE4E1', + moccasin: '#FFE4B5', + navajowhite: '#FFDEAD', + oldlace: '#FDF5E6', + olivedrab: '#6B8E23', + orangered: '#FF4500', + orchid: '#DA70D6', + palegoldenrod: '#EEE8AA', + palegreen: '#98FB98', + paleturquoise: '#AFEEEE', + palevioletred: '#DB7093', + papayawhip: '#FFEFD5', + peachpuff: '#FFDAB9', + peru: '#CD853F', + pink: '#FFC0CB', + plum: '#DDA0DD', + powderblue: '#B0E0E6', + rosybrown: '#BC8F8F', + royalblue: '#4169E1', + saddlebrown: '#8B4513', + salmon: '#FA8072', + sandybrown: '#F4A460', + seagreen: '#2E8B57', + seashell: '#FFF5EE', + sienna: '#A0522D', + skyblue: '#87CEEB', + slateblue: '#6A5ACD', + slategray: '#708090', + slategrey: '#708090', + snow: '#FFFAFA', + springgreen: '#00FF7F', + steelblue: '#4682B4', + tan: '#D2B48C', + thistle: '#D8BFD8', + tomato: '#FF6347', + turquoise: '#40E0D0', + violet: '#EE82EE', + wheat: '#F5DEB3', + whitesmoke: '#F5F5F5', + yellowgreen: '#9ACD32', + rebeccapurple: '#663399' + }.freeze + + def convert_color_name_to_hex + color = params[:color] + color_name = color.strip.downcase + + return color if color_name.start_with?('#') + + COLOR_NAME_TO_HEX[color_name.to_sym] || color + end + end +end diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb new file mode 100644 index 00000000000..6c399c92377 --- /dev/null +++ b/app/services/labels/create_service.rb @@ -0,0 +1,25 @@ +module Labels + class CreateService < Labels::BaseService + def initialize(params = {}) + @params = params.dup.with_indifferent_access + end + + # returns the created label + def execute(target_params) + params[:color] = convert_color_name_to_hex if params[:color].present? + + project_or_group = target_params[:project] || target_params[:group] + + if project_or_group.present? + project_or_group.labels.create(params) + elsif target_params[:template] + label = Label.new(params) + label.template = true + label.save + label + else + Rails.logger.warn("target_params should contain :project or :group or :template, actual value: #{target_params}") + end + end + end +end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index cf4f7606c94..940c8b333d3 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -3,7 +3,7 @@ module Labels def initialize(current_user, project, params = {}) @current_user = current_user @project = project - @params = params.dup + @params = params.dup.with_indifferent_access end def execute(skip_authorization: false) @@ -28,7 +28,7 @@ module Labels new_label = available_labels.find_by(title: title) if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project)) - new_label = project.labels.create(params) + new_label = Labels::CreateService.new(params).execute(project: project) end new_label diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb new file mode 100644 index 00000000000..28dcabf9541 --- /dev/null +++ b/app/services/labels/update_service.rb @@ -0,0 +1,15 @@ +module Labels + class UpdateService < Labels::BaseService + def initialize(params = {}) + @params = params.dup.with_indifferent_access + end + + # returns the updated label + def execute(label) + params[:color] = convert_color_name_to_hex if params[:color].present? + + label.update(params) + label + end + end +end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index fdce542bd9e..d45da5180e1 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -21,7 +21,9 @@ module MergeRequests delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request def find_source_project - source_project || project + return source_project if source_project.present? && can?(current_user, :read_project, source_project) + + project end def find_target_project diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index 3cb9aae83f6..ab7fcf3b6e2 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -28,7 +28,7 @@ module MergeRequests if merge_request.previous_changes.include?('title') || merge_request.previous_changes.include?('description') - todo_service.update_merge_request(merge_request, current_user) + todo_service.update_merge_request(merge_request, current_user, old_mentioned_users) end if merge_request.previous_changes.include?('target_branch') diff --git a/app/services/note_summary.rb b/app/services/note_summary.rb new file mode 100644 index 00000000000..a6f6320d573 --- /dev/null +++ b/app/services/note_summary.rb @@ -0,0 +1,20 @@ +class NoteSummary + attr_reader :note + attr_reader :metadata + + def initialize(noteable, project, author, body, action: nil, commit_count: nil) + @note = { noteable: noteable, project: project, author: author, note: body } + @metadata = { action: action, commit_count: commit_count }.compact + + set_commit_params if note[:noteable].is_a?(Commit) + end + + def metadata? + metadata.present? + end + + def set_commit_params + note.merge!(noteable_type: 'Commit', commit_id: note[:noteable].id) + note[:noteable] = nil + end +end diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb index 75a4b3ed826..75fd08ea0a9 100644 --- a/app/services/notes/update_service.rb +++ b/app/services/notes/update_service.rb @@ -3,11 +3,13 @@ module Notes def execute(note) return note unless note.editable? + old_mentioned_users = note.mentioned_users.to_a + note.update_attributes(params.merge(updated_by: current_user)) note.create_new_cross_references!(current_user) if note.previous_changes.include?('note') - TodoService.new.update_note(note, current_user) + TodoService.new.update_note(note, current_user, old_mentioned_users) end note diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 44ae23fad18..8bb995158de 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -3,7 +3,7 @@ # class NotificationRecipientService attr_reader :project - + def initialize(project) @project = project end @@ -12,11 +12,7 @@ class NotificationRecipientService custom_action = build_custom_key(action, target) recipients = target.participants(current_user) - - unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) - recipients = add_project_watchers(recipients) - end - + recipients = add_project_watchers(recipients) recipients = add_custom_notifications(recipients, custom_action) recipients = reject_mention_users(recipients) @@ -38,16 +34,38 @@ class NotificationRecipientService recipients = reject_unsubscribed_users(recipients, target) recipients = reject_users_without_access(recipients, target) - recipients.delete(current_user) if skip_current_user + recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity? recipients.uniq end + def build_pipeline_recipients(target, current_user, action:) + return [] unless current_user + + custom_action = + case action.to_s + when 'failed' + :failed_pipeline + when 'success' + :success_pipeline + end + + notification_setting = notification_setting_for_user_project(current_user, target.project) + + return [] if notification_setting.mention? || notification_setting.disabled? + + return [] if notification_setting.custom? && !notification_setting.public_send(custom_action) + + return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + + reject_users_without_access([current_user], target) + end + def build_relabeled_recipients(target, current_user, labels:) recipients = add_labels_subscribers([], target, labels: labels) recipients = reject_unsubscribed_users(recipients, target) recipients = reject_users_without_access(recipients, target) - recipients.delete(current_user) + recipients.delete(current_user) unless current_user.notified_of_own_activity? recipients.uniq end @@ -88,7 +106,7 @@ class NotificationRecipientService recipients = reject_unsubscribed_users(recipients, note.noteable) recipients = reject_users_without_access(recipients, note.noteable) - recipients.delete(note.author) + recipients.delete(note.author) unless note.author.notified_of_own_activity? recipients.uniq end @@ -290,4 +308,16 @@ class NotificationRecipientService def build_custom_key(action, object) "#{action}_#{object.class.model_name.name.underscore}".to_sym end + + def notification_setting_for_user_project(user, project) + project_setting = user.notification_settings_for(project) + + return project_setting unless project_setting.global? + + group_setting = user.notification_settings_for(project.group) + + return group_setting unless group_setting.global? + + user.global_notification_setting + end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index f9aa2346759..6b186263bd1 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -278,10 +278,11 @@ class NotificationService return unless mailer.respond_to?(email_template) - recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients( + recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients( pipeline, - nil, # The acting user, who won't be added to recipients - action: pipeline.status).map(&:notification_email) + pipeline.user, + action: pipeline.status, + ).map(&:notification_email) if recipients.any? mailer.public_send(email_template, pipeline, recipients).deliver_later diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index d484a96f785..4c72d5e117d 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -11,7 +11,7 @@ module Projects success rescue => e - error(e.message) + error("Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}") end private @@ -32,23 +32,40 @@ module Projects end def import_repository + raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url) + begin - raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url) - gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url) - rescue => e + if project.github_import? || project.gitea_import? + fetch_repository + else + clone_repository + end + rescue Gitlab::Shell::Error => e # Expire cache to prevent scenarios such as: # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true # 2. Retried import, repo is broken or not imported but +exists?+ still returns true - project.repository.before_import if project.repository_exists? + project.repository.expire_content_cache if project.repository_exists? - raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" + raise Error, e.message end end + def clone_repository + gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url) + end + + def fetch_repository + project.create_repository + project.repository.add_remote(project.import_type, project.import_url) + project.repository.set_remote_as_mirror(project.import_type) + project.repository.fetch_remote(project.import_type, forced: true) + project.repository.remove_remote(project.import_type) + end + def import_data return unless has_importer? - project.repository.before_import unless project.gitlab_project_import? + project.repository.expire_content_cache unless project.gitlab_project_import? unless importer.execute raise Error, 'The remote data could not be imported.' diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 523b9f41916..17cf71cf098 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -46,6 +46,7 @@ module Projects end def error(message, http_status = nil) + log_error("Projects::UpdatePagesService: #{message}") @status.allow_failure = !latest? @status.description = message @status.drop diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index 781cd13b44b..c1549df5ac6 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -16,5 +16,13 @@ module Search Gitlab::SearchResults.new(current_user, projects, params[:search]) end + + def scope + @scope ||= begin + allowed_scopes = %w[issues merge_requests milestones] + + allowed_scopes.delete(params[:scope]) { 'projects' } + end + end end end diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb index 4b500914cfb..9a22abae635 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -12,5 +12,9 @@ module Search params[:search], params[:repository_ref]) end + + def scope + @scope ||= %w[notes issues merge_requests milestones wiki_blobs commits].delete(params[:scope]) { 'blobs' } + end end end diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb index 0b3e713e220..4f161beea4d 100644 --- a/app/services/search/snippet_service.rb +++ b/app/services/search/snippet_service.rb @@ -11,5 +11,9 @@ module Search Gitlab::SnippetSearchResults.new(snippets, params[:search]) end + + def scope + @scope ||= %w[snippet_titles].delete(params[:scope]) { 'snippet_blobs' } + end end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb new file mode 100644 index 00000000000..8d46a8dab3e --- /dev/null +++ b/app/services/search_service.rb @@ -0,0 +1,63 @@ +class SearchService + include Gitlab::Allowable + + def initialize(current_user, params = {}) + @current_user = current_user + @params = params.dup + end + + def project + return @project if defined?(@project) + + @project = + if params[:project_id].present? + the_project = Project.find_by(id: params[:project_id]) + can?(current_user, :download_code, the_project) ? the_project : nil + else + nil + end + end + + def group + return @group if defined?(@group) + + @group = + if params[:group_id].present? + the_group = Group.find_by(id: params[:group_id]) + can?(current_user, :read_group, the_group) ? the_group : nil + else + nil + end + end + + def show_snippets? + return @show_snippets if defined?(@show_snippets) + + @show_snippets = params[:snippets] == 'true' + end + + delegate :scope, to: :search_service + + def search_results + @search_results ||= search_service.execute + end + + def search_objects + @search_objects ||= search_results.objects(scope, params[:page]) + end + + private + + def search_service + @search_service ||= + if project + Search::ProjectService.new(project, current_user, params) + elsif show_snippets? + Search::SnippetService.new(current_user, params) + else + Search::GlobalService.new(current_user, params) + end + end + + attr_reader :current_user, :params +end diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb index 023e0824e85..11030bee8f1 100644 --- a/app/services/spam_check_service.rb +++ b/app/services/spam_check_service.rb @@ -14,6 +14,9 @@ module SpamCheckService @spam_log_id = params.delete(:spam_log_id) end + # In order to be proceed to the spam check process, @spammable has to be + # a dirty instance, which means it should be already assigned with the new + # attribute values. def spam_check(spammable, user) spam_service = SpamService.new(spammable, @request) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 8e02fe3741a..35cfcc3682e 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -26,7 +26,7 @@ module SystemNoteService body << new_commit_summary(new_commits).join("\n") body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})" - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count)) end # Called when the assignee of a Noteable is changed or removed @@ -46,7 +46,7 @@ module SystemNoteService def change_assignee(noteable, project, author, assignee) body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}" - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee')) end # Called when one or more labels on a Noteable are added and/or removed @@ -86,7 +86,7 @@ module SystemNoteService body << ' ' << 'label'.pluralize(labels_count) - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'label')) end # Called when the milestone of a Noteable is changed @@ -106,7 +106,7 @@ module SystemNoteService def change_milestone(noteable, project, author, milestone) body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project)}" - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone')) end # Called when the estimated time of a Noteable is changed @@ -132,7 +132,7 @@ module SystemNoteService "changed time estimate to #{parsed_time}" end - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) end # Called when the spent time of a Noteable is changed @@ -161,7 +161,7 @@ module SystemNoteService body = "#{action} #{parsed_time} of time spent" end - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) end # Called when the status of a Noteable is changed @@ -183,53 +183,59 @@ module SystemNoteService body = status.dup body << " via #{source.gfm_reference(project)}" if source - create_note(noteable: noteable, project: project, author: author, note: body) + action = status == 'reopened' ? 'opened' : status + + create_note(NoteSummary.new(noteable, project, author, body, action: action)) end # Called when 'merge when pipeline succeeds' is executed def merge_when_pipeline_succeeds(noteable, project, author, last_commit) body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds" - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) end # Called when 'merge when pipeline succeeds' is canceled def cancel_merge_when_pipeline_succeeds(noteable, project, author) body = 'canceled the automatic merge' - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'merge')) end def remove_merge_request_wip(noteable, project, author) body = 'unmarked as a **Work In Progress**' - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) end def add_merge_request_wip(noteable, project, author) body = 'marked as a **Work In Progress**' - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) end def add_merge_request_wip_from_commit(noteable, project, author, commit) body = "marked as a **Work In Progress** from #{commit.to_reference(project)}" - create_note(noteable: noteable, project: project, author: author, note: body) + create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) end def self.resolve_all_discussions(merge_request, project, author) body = "resolved all discussions" - create_note(noteable: merge_request, project: project, author: author, note: body) + create_note(NoteSummary.new(merge_request, project, author, body, action: 'discussion')) end def discussion_continued_in_issue(discussion, project, author, issue) body = "created #{issue.to_reference} to continue this discussion" - note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body) - note_attributes[:type] = note_attributes.delete(:note_type) - create_note(note_attributes) + note_params = discussion.reply_attributes.merge(project: project, author: author, note: body) + note_params[:type] = note_params.delete(:note_type) + + note = Note.create(note_params.merge(system: true)) + note.system_note_metadata = SystemNoteMetadata.new({ action: 'discussion' }) + + note end # Called when the title of a Noteable is changed @@ -253,7 +259,8 @@ module SystemNoteService marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true) body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**" - create_note(noteable: noteable, project: project, author: author, note: body) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) end # Called when the confidentiality changes @@ -268,8 +275,15 @@ module SystemNoteService # # Returns the created Note object def change_issue_confidentiality(issue, project, author) - body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone' - create_note(noteable: issue, project: project, author: author, note: body) + if issue.confidential + body = 'made the issue confidential' + action = 'confidential' + else + body = 'made the issue visible to everyone' + action = 'visible' + end + + create_note(NoteSummary.new(issue, project, author, body, action: action)) end # Called when a branch in Noteable is changed @@ -288,7 +302,8 @@ module SystemNoteService # Returns the created Note object def change_branch(noteable, project, author, branch_type, old_branch, new_branch) body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`" - create_note(noteable: noteable, project: project, author: author, note: body) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'branch')) end # Called when a branch in Noteable is added or deleted @@ -314,7 +329,8 @@ module SystemNoteService end body = "#{verb} #{branch_type} branch `#{branch}`" - create_note(noteable: noteable, project: project, author: author, note: body) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'branch')) end # Called when a branch is created from the 'new branch' button on a issue @@ -325,7 +341,8 @@ module SystemNoteService link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch) body = "created branch [`#{branch}`](#{link})" - create_note(noteable: issue, project: project, author: author, note: body) + + create_note(NoteSummary.new(issue, project, author, body, action: 'branch')) end # Called when a Mentionable references a Noteable @@ -349,23 +366,12 @@ module SystemNoteService return if cross_reference_disallowed?(noteable, mentioner) gfm_reference = mentioner.gfm_reference(noteable.project) - - note_options = { - project: noteable.project, - author: author, - note: cross_reference_note_content(gfm_reference) - } - - if noteable.is_a?(Commit) - note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id) - else - note_options[:noteable] = noteable - end + body = cross_reference_note_content(gfm_reference) if noteable.is_a?(ExternalIssue) noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author) else - create_note(note_options) + create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference')) end end @@ -444,7 +450,8 @@ module SystemNoteService def change_task_status(noteable, project, author, new_task) status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE body = "marked the task **#{new_task.source}** as #{status_label}" - create_note(noteable: noteable, project: project, author: author, note: body) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'task')) end # Called when noteable has been moved to another project @@ -466,7 +473,8 @@ module SystemNoteService cross_reference = noteable_ref.to_reference(project) body = "moved #{direction} #{cross_reference}" - create_note(noteable: noteable, project: project, author: author, note: body) + + create_note(NoteSummary.new(noteable, project, author, body, action: 'moved')) end private @@ -482,8 +490,11 @@ module SystemNoteService end end - def create_note(args = {}) - Note.create(args.merge(system: true)) + def create_note(note_summary) + note = Note.create(note_summary.note.merge(system: true)) + note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata? + + note end def cross_reference_note_prefix diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index bf7e76ec59e..b6e88b0280f 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -19,8 +19,8 @@ class TodoService # # * mark all pending todos related to the issue for the current user as done # - def update_issue(issue, current_user) - update_issuable(issue, current_user) + def update_issue(issue, current_user, skip_users = []) + update_issuable(issue, current_user, skip_users) end # When close an issue we should: @@ -60,8 +60,8 @@ class TodoService # # * create a todo for each mentioned user on merge request # - def update_merge_request(merge_request, current_user) - update_issuable(merge_request, current_user) + def update_merge_request(merge_request, current_user, skip_users = []) + update_issuable(merge_request, current_user, skip_users) end # When close a merge request we should: @@ -123,7 +123,7 @@ class TodoService mark_pending_todos_as_done(merge_request, merge_request.author) mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? end - + # When a merge request could not be automatically merged due to its unmergeable state we should: # # * create a todo for a merge_user @@ -131,7 +131,7 @@ class TodoService def merge_request_became_unmergeable(merge_request) create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds? end - + # When create a note we should: # # * mark all pending todos related to the noteable for the note author as done @@ -146,8 +146,8 @@ class TodoService # * mark all pending todos related to the noteable for the current user as done # * create a todo for each new user mentioned on note # - def update_note(note, current_user) - handle_note(note, current_user) + def update_note(note, current_user, skip_users = []) + handle_note(note, current_user, skip_users) end # When an emoji is awarded we should: @@ -204,7 +204,7 @@ class TodoService # Only update those that are not really on that state todos = todos.where.not(state: state) todos_ids = todos.pluck(:id) - todos.update_all(state: state) + todos.unscope(:order).update_all(state: state) current_user.update_todos_count_cache todos_ids end @@ -223,11 +223,11 @@ class TodoService create_mention_todos(issuable.project, issuable, author) end - def update_issuable(issuable, author) + def update_issuable(issuable, author, skip_users = []) # Skip toggling a task list item in a description return if toggling_tasks?(issuable) - create_mention_todos(issuable.project, issuable, author) + create_mention_todos(issuable.project, issuable, author, nil, skip_users) end def destroy_issuable(issuable, user) @@ -239,7 +239,7 @@ class TodoService issuable.tasks? && issuable.updated_tasks.any? end - def handle_note(note, author) + def handle_note(note, author, skip_users = []) # Skip system notes, and notes on project snippet return if note.system? || note.for_snippet? @@ -247,7 +247,7 @@ class TodoService target = note.noteable mark_pending_todos_as_done(target, author) - create_mention_todos(project, target, author, note) + create_mention_todos(project, target, author, note, skip_users) end def create_assignment_todo(issuable, author) @@ -257,14 +257,14 @@ class TodoService end end - def create_mention_todos(project, target, author, note = nil) + def create_mention_todos(project, target, author, note = nil, skip_users = []) # Create Todos for directly addressed users - directly_addressed_users = filter_directly_addressed_users(project, note || target, author) + directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users) attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note) create_todos(directly_addressed_users, attributes) # Create Todos for mentioned users - mentioned_users = filter_mentioned_users(project, note || target, author) + mentioned_users = filter_mentioned_users(project, note || target, author, skip_users) attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note) create_todos(mentioned_users, attributes) end @@ -307,13 +307,13 @@ class TodoService reject_users_without_access(users, project, target).uniq end - def filter_mentioned_users(project, target, author) - mentioned_users = target.mentioned_users(author) + def filter_mentioned_users(project, target, author, skip_users = []) + mentioned_users = target.mentioned_users(author) - skip_users filter_todo_users(mentioned_users, project, target) end - def filter_directly_addressed_users(project, target, author) - directly_addressed_users = target.directly_addressed_users(author) + def filter_directly_addressed_users(project, target, author, skip_users = []) + directly_addressed_users = target.directly_addressed_users(author) - skip_users filter_todo_users(directly_addressed_users, project, target) end diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb new file mode 100644 index 00000000000..a847a71a66a --- /dev/null +++ b/app/services/users/create_service.rb @@ -0,0 +1,112 @@ +module Users + # Service for creating a new user. + class CreateService < BaseService + def initialize(current_user, params = {}) + @current_user = current_user + @params = params.dup + end + + def build + raise Gitlab::Access::AccessDeniedError unless can_create_user? + + user = User.new(build_user_params) + + if current_user&.is_admin? + if params[:reset_password] + @reset_token = user.generate_reset_token + params[:force_random_password] = true + end + + if params[:force_random_password] + random_password = Devise.friendly_token.first(Devise.password_length.min) + user.password = user.password_confirmation = random_password + end + end + + identity_attrs = params.slice(:extern_uid, :provider) + + if identity_attrs.any? + user.identities.build(identity_attrs) + end + + user + end + + def execute + user = build + + if user.save + log_info("User \"#{user.name}\" (#{user.email}) was created") + notification_service.new_user(user, @reset_token) if @reset_token + system_hook_service.execute_hooks_for(user, :create) + end + + user + end + + private + + def can_create_user? + (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.is_admin? + end + + # Allowed params for creating a user (admins only) + def admin_create_params + [ + :access_level, + :admin, + :avatar, + :bio, + :can_create_group, + :color_scheme_id, + :email, + :external, + :force_random_password, + :password_automatically_set, + :hide_no_password, + :hide_no_ssh_key, + :key_id, + :linkedin, + :name, + :password, + :password_expires_at, + :projects_limit, + :remember_me, + :skip_confirmation, + :skype, + :theme_id, + :twitter, + :username, + :website_url + ] + end + + # Allowed params for user signup + def signup_params + [ + :email, + :email_confirmation, + :password_automatically_set, + :name, + :password, + :username + ] + end + + def build_user_params + if current_user&.is_admin? + user_params = params.slice(*admin_create_params) + user_params[:created_by_id] = current_user&.id + + if params[:reset_password] + user_params.merge!(force_random_password: true, password_expires_at: nil) + end + else + user_params = params.slice(*signup_params) + user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email + end + + user_params + end + end +end diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 833da5bc5d1..a3b32a71a64 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -20,10 +20,10 @@ module Users Groups::DestroyService.new(group, current_user).execute end - user.personal_projects.each do |project| + user.personal_projects.with_deleted.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute end move_issues_to_ghost_user(user) diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 3eab065bb9f..5d51a2b5cbc 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -558,5 +558,19 @@ Maximum time for web terminal websocket connection (in seconds). 0 for unlimited. + %fieldset + %legend Real-time features + .form-group + = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :polling_interval_multiplier, class: 'form-control' + .help-block + Change this value to influence how frequently the GitLab UI polls for updates. + If you set the value to 2 all polling intervals are multiplied + by 2, which means that polling happens half as frequently. + The multiplier can also have a decimal value. + The default value (1) is a reasonable choice for the majority of GitLab + installations. Set to 0 to completely disable polling. + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index e67ad663720..ebca9beb035 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -43,28 +43,34 @@ %h4 Features %hr - %p - Sign up + - sign_up = "Sign up" + %p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") } + = sign_up %span.light.pull-right = boolean_to_icon signup_enabled? - %p - LDAP + - ldap = "LDAP" + %p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") } + = ldap %span.light.pull-right = boolean_to_icon Gitlab.config.ldap.enabled - %p - Gravatar + - gravatar = "Gravatar" + %p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") } + = gravatar %span.light.pull-right = boolean_to_icon gravatar_enabled? - %p - OmniAuth + - omniauth = "OmniAuth" + %p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") } + = omniauth %span.light.pull-right = boolean_to_icon Gitlab.config.omniauth.enabled - %p - Reply by email + - reply_email = "Reply by email" + %p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") } + = reply_email %span.light.pull-right = boolean_to_icon Gitlab::IncomingEmail.enabled? - %p - Container Registry + - container_reg = "Container Registry" + %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") } + = container_reg %span.light.pull-right = boolean_to_icon Gitlab.config.registry.enabled diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml index c1a9f8d6ddd..596f367a00d 100644 --- a/app/views/admin/projects/_projects.html.haml +++ b/app/views/admin/projects/_projects.html.haml @@ -1,15 +1,16 @@ .js-projects-list-holder - if @projects.any? - %ul.projects-list.content-list + %ul.projects-list.content-list.admin-projects - @projects.each_with_index do |project| - %li.project-row + %li.project-row{ class: ('no-description' if project.description.blank?) } .controls - - if project.archived - %span.label.label-warning archived - %span.badge - = storage_counter(project.statistics.storage_size) = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn" = link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove" + .stats + %span.badge + = storage_counter(project.statistics.storage_size) + - if project.archived + %span.label.label-warning archived .title = link_to [:admin, project.namespace.becomes(Namespace), project] do .dash-project-avatar @@ -20,7 +21,7 @@ - if project.namespace = project.namespace.human_name \/ - %span.project-name.filter-title + %span.project-name = project.name - if project.description.present? diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 7855239dfe5..794aaec89bd 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -2,7 +2,7 @@ %legend Access .form-group = f.label :projects_limit, class: 'control-label' - .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control' + .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control' .form-group = f.label :can_create_group, class: 'control-label' diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index a1ef34dc588..5aae410a63f 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -10,8 +10,8 @@ - if current_user .award-menu-holder.js-award-holder - %button.btn.award-control.js-add-award{ type: "button" } + %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', + 'aria-label': 'Add emoji', + data: { title: 'Add emoji', placement: "bottom" } } = icon('smile-o', class: "award-control-icon award-control-icon-normal") = icon('spinner spin', class: "award-control-icon award-control-icon-loading") - %span.award-control-text - Add diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml index 0530d21a7e2..128b418090f 100644 --- a/app/views/ci/status/_graph_badge.html.haml +++ b/app/views/ci/status/_graph_badge.html.haml @@ -6,7 +6,7 @@ - tooltip = "#{subject.name} - #{status.label}" - if status.has_details? - = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip } do + = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do %span{ class: klass }= custom_icon(status.icon) .ci-status-text= subject.name - else @@ -15,6 +15,6 @@ .ci-status-text= subject.name - if status.has_action? - = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do + = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do %i.ci-action-icon-wrapper{ class: "js-#{status.action_icon.dasherize}" } = custom_icon(status.action_icon) diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 600ee63a5c0..4679b9549d1 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -1,7 +1,9 @@ = content_for :flash_message do = render 'shared/project_limit' -.top-area - %ul.nav-links +.top-area.scrolling-tabs-container.inner-page-scroll-tabs + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.nav-links.scrolling-tabs = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do Your projects diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml index 60c84a26420..2129920afd2 100644 --- a/app/views/dashboard/milestones/show.html.haml +++ b/app/views/dashboard/milestones/show.html.haml @@ -1,5 +1,5 @@ - header_title "Milestones", dashboard_milestones_path = render 'shared/milestones/top', milestone: @milestone -= render 'shared/milestones/summary', milestone: @milestone = render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true += render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 51 diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index eef794dbd51..596499230f9 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -4,7 +4,9 @@ - page_title "Projects" - header_title "Projects", dashboard_projects_path -.user-callout{ 'callout-svg' => custom_icon('icon_customization') } +- unless show_user_callout? + = render 'shared/user_callout' + - if @projects.any? || params[:name] = render 'dashboard/projects_head' diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index d31ced004a0..52d6ebd8a14 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -19,12 +19,13 @@ .nav-controls - if @todos.any?(&:pending?) - = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do - Mark all as done - = icon('spinner spin') - = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do - Undo mark all as done - = icon('spinner spin') + .append-right-default + = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do + Mark all as done + = icon('spinner spin') + = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do + Undo mark all as done + = icon('spinner spin') .todos-filters .row-content-block.second-block @@ -67,12 +68,11 @@ = link_to todos_filter_path(sort: sort_value_oldest_created) do = sort_title_oldest_created - .js-todos-all - if @todos.any? .js-todos-list-container .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } } - .panel.panel-default.panel-small.panel-without-border + .panel.panel-default.panel-without-border.panel-without-margin %ul.content-list.todos-list = render @todos = paginate @todos, theme: "gitlab" diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 5d359538efe..21c751a23f8 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -8,7 +8,7 @@ - if devise_mapping.rememberable? .remember-me.checkbox %label{ for: "user_remember_me" } - = f.check_box :remember_me + = f.check_box :remember_me, class: 'remember-me-checkbox' %span Remember me .pull-right.forgot-password = link_to "Forgot your password?", new_password_path(resource_name) diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml index 6f5d4bf2a2f..2d78c55211e 100644 --- a/app/views/discussions/_discussion.html.haml +++ b/app/views/discussions/_discussion.html.haml @@ -8,7 +8,7 @@ .discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } } .discussion-header .discussion-actions - = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do + %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button" } - if expanded = icon("chevron-up") - else diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 56f463572bb..f630f1effdc 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -17,24 +17,3 @@ = link_to filter_projects_path(visibility_level: level) do = visibility_level_icon(level) = visibility_level_label(level) - -- if @tags.present? - .dropdown - %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } - = icon('tags') - %span.light Tags: - - if params[:tag].present? - = params[:tag] - - else - Any - = icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-align-right - %li - = link_to filter_projects_path(tag: nil) do - Any - - - @tags.each do |tag| - %li{ class: active_when(tag.name == params[:tag]) || 'light' } - = link_to filter_projects_path(tag: tag.name) do - = icon('tag') - = tag.name diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index 6ad76d23df5..8fe0bd149f3 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -1,18 +1,22 @@ - page_title "Merge Requests" -.top-area - = render 'shared/issuable/nav', type: :merge_requests - - if current_user - .nav-controls - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" +- if @group_merge_requests.empty? + = render 'shared/empty_states/merge_requests', project_select_button: true +- else + .top-area + = render 'shared/issuable/nav', type: :merge_requests + - if current_user + .nav-controls + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request" -= render 'shared/issuable/filter', type: :merge_requests + = render 'shared/issuable/filter', type: :merge_requests -.row-content-block.second-block - Only merge requests from - %strong= @group.name - group are listed here. - - if current_user - To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. + .row-content-block.second-block + Only merge requests from + %strong= @group.name + group are listed here. + - if current_user + To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. -= render 'shared/merge_requests' + .prepend-top-default + = render 'shared/merge_requests' diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml index e66a8e0a3b3..33e68bc766e 100644 --- a/app/views/groups/milestones/show.html.haml +++ b/app/views/groups/milestones/show.html.haml @@ -1,8 +1,4 @@ = render "header_title" - -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test? - = render 'shared/milestones/top', milestone: @milestone, group: @group -= render 'shared/milestones/summary', milestone: @milestone = render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true += render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102 diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 2684f16c373..8e6da3fad90 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -118,6 +118,12 @@ .key m %td Go to merge requests + %tr + %td.shortcut + .key g + .key t + %td + Go to todos %tbody %tr %th diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index a35a918d501..b7df11681d3 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -3,8 +3,9 @@ .layout-nav .container-fluid = render "layouts/nav/#{nav}" - .content-wrapper{ class: "#{layout_nav_class}" } + - if content_for?(:sub_nav) = yield :sub_nav + .content-wrapper{ class: layout_nav_class } .alert-wrapper = render "layouts/broadcast" = render "layouts/flash" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 5fde5c2613e..23abf6897d4 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -11,9 +11,13 @@ = render 'layouts/nav/dashboard' - else = render 'layouts/nav/explore' - %button.navbar-toggle{ type: 'button' } - %span.sr-only Toggle navigation - = icon('ellipsis-v') + + .header-logo + = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do + = brand_header_logo + + .title-container + %h1.title{ class: ('initializing' if @has_group_title) }= title .navbar-collapse.collapse %ul.nav.navbar-nav @@ -31,11 +35,6 @@ %li = link_to admin_root_path, title: 'Admin Area', aria: { label: "Admin Area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('wrench fw') - %li - = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do - = icon('bell fw') - %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) } - = todos_count_format(todos_pending_count) - if current_user.can_create_project? %li = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do @@ -45,6 +44,21 @@ = link_to sherlock_transactions_path, title: 'Sherlock Transactions', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('tachometer fw') + %li + = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('hashtag fw') + %span.badge.issues-count + = number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened)) + %li + = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = custom_icon('mr_bold') + %span.badge.merge-requests-count + = number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened)) + %li + = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = icon('check-circle fw') + %span.badge.todos-count + = todos_count_format(todos_pending_count) %li.header-user.dropdown = link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do = image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar" @@ -63,11 +77,9 @@ %div = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success' - .header-logo - = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do - = brand_header_logo - - %h1.title{ class: ('initializing' if @has_group_title) }= title + %button.navbar-toggle{ type: 'button' } + %span.sr-only Toggle navigation + = icon('ellipsis-v') = yield :header_content diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index 85a1aea3a61..4beb6fcee5d 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -3,8 +3,8 @@ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } %tbody %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" } - %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;line-height:1;" } + %img{ alt: "โ", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" } Your pipeline has failed. %tr.spacer @@ -16,7 +16,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;" } - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } @@ -26,7 +26,7 @@ = @project.name %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr @@ -37,7 +37,7 @@ = @pipeline.ref %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:400;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr @@ -52,13 +52,13 @@ = @merge_request.to_reference .commit{ style: "color:#5c5c5c;font-weight:300;" } = @pipeline.git_commit_message.truncate(50) + - commit = @pipeline.commit %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - - commit = @pipeline.commit %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } @@ -68,15 +68,48 @@ - else %span = commit.author_name + - if commit.different_committer? + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Committed by + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + - if commit.committer + %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } + = commit.committer.name + - else + %span + = commit.committer_name + %tr.spacer %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } -- failed = @pipeline.statuses.latest.failed %tr.pre-section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" } - Pipeline - %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = "\##{@pipeline.id}" + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px 0 5px;text-align:center;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } + Pipeline + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = "\##{@pipeline.id}" + triggered by + - if @pipeline.user + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } + %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } + %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } + = @pipeline.user.name + - else + %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" } + API +- failed = @pipeline.statuses.latest.failed +%tr + %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" } had = failed.size failed @@ -94,8 +127,8 @@ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" } - %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#d22f57;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;line-height:10px" } + %img{ alt: "โ", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" } = build.stage %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb index 520a2fc7d68..c1a4ea40cf5 100644 --- a/app/views/notify/pipeline_failed_email.text.erb +++ b/app/views/notify/pipeline_failed_email.text.erb @@ -14,9 +14,21 @@ Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) <% else -%> Commit Author: <%= commit.author_name %> <% end -%> +<% if commit.different_committer? -%> +<% if commit.committer -%> +Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> ) +<% else -%> +Committed by: <%= commit.committer_name %> +<% end -%> +<% end -%> +<% if @pipeline.user -%> +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) +<% else -%> +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API +<% end -%> <% failed = @pipeline.statuses.latest.failed -%> -Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. +had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. <% failed.each do |build| -%> <%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %> diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 19d4add06f5..9c2e2a599b2 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -16,7 +16,7 @@ %tbody %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;" } - namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name - namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner) %a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" } @@ -26,7 +26,7 @@ = @project.name %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr @@ -37,7 +37,7 @@ = @pipeline.ref %tr %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:400;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr @@ -52,13 +52,13 @@ = @merge_request.to_reference .commit{ style: "color:#5c5c5c;font-weight:300;" } = @pipeline.git_commit_message.truncate(50) + - commit = @pipeline.commit %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit Author + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" } %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } %tbody %tr - - commit = @pipeline.commit %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } @@ -68,17 +68,50 @@ - else %span = commit.author_name + - if commit.different_committer? + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Committed by + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } + %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" } + - if commit.committer + %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } + = commit.committer.name + - else + %span + = commit.committer_name + %tr.spacer %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" } %tr.success-message - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" } - - build_count = @pipeline.statuses.latest.size + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px 0 5px;text-align:center;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } + Pipeline + %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } + = "\##{@pipeline.id}" + triggered by + - if @pipeline.user + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" } + %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/ + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" } + %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" } + = @pipeline.user.name + - else + %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" } + API +%tr + %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" } + - job_count = @pipeline.statuses.latest.size - stage_count = @pipeline.stages_count - Pipeline - %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } - = "\##{@pipeline.id}" successfully completed - #{build_count} #{'build'.pluralize(build_count)} + #{job_count} #{'job'.pluralize(job_count)} in #{stage_count} #{'stage'.pluralize(stage_count)}. diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb index 0970a3a4e09..ddced2279e1 100644 --- a/app/views/notify/pipeline_success_email.text.erb +++ b/app/views/notify/pipeline_success_email.text.erb @@ -14,7 +14,19 @@ Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) <% else -%> Commit Author: <%= commit.author_name %> <% end -%> +<% if commit.different_committer? -%> +<% if commit.committer -%> +Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> ) +<% else -%> +Committed by: <%= commit.committer_name %> +<% end -%> +<% end -%> <% build_count = @pipeline.statuses.latest.size -%> <% stage_count = @pipeline.stages_count -%> -Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. +<% if @pipeline.user -%> +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) +<% else -%> +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API +<% end -%> +successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml index b28fea35ad5..76440926a2b 100644 --- a/app/views/notify/project_was_exported_email.html.haml +++ b/app/views/notify/project_was_exported_email.html.haml @@ -2,7 +2,7 @@ Project #{@project.name} was exported successfully. %p The project export can be downloaded from: - = link_to download_export_namespace_project_url(@project.namespace, @project) do + = link_to download_export_namespace_project_url(@project.namespace, @project), rel: 'nofollow', download: '', do = @project.name_with_namespace + " export" %p The download link will expire in 24 hours. diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 8a994f6d600..5ce2220c907 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -75,12 +75,12 @@ .provider-btn-image = provider_image_tag(provider) - if auth_active?(provider) - - if provider.to_s == 'saml' - %a.provider-btn - Active - - else + - if unlink_allowed?(provider) = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do Disconnect + - else + %a.provider-btn + Active - else = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do Connect diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 5c5e5940365..51c4e8e5a73 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -34,6 +34,11 @@ .clearfix + = form_for @user, url: profile_notifications_path, method: :put do |f| + %label{ for: 'user_notified_of_own_activity' } + = f.check_box :notified_of_own_activity + %span Receive notifications about your own activity + %hr %h5 Groups (#{@group_notifications.count}) diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index df0a0212f3d..99690e6b98a 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -6,7 +6,9 @@ %h4.prepend-top-0 Syntax highlighting theme %p - This setting allow you to customize the appearance of the syntax. + This setting allows you to customize the appearance of the syntax. + = succeed '.' do + = link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank' .col-lg-9.syntax-theme - Gitlab::ColorSchemes.each do |scheme| = label_tag do @@ -20,6 +22,8 @@ Behavior %p This setting allows you to customize the behavior of the system layout and default views. + = succeed '.' do + = link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank' .col-lg-9 .form-group = f.label :layout, class: 'label-light' do @@ -29,13 +33,11 @@ Choose between fixed (max. 1200px) and fluid (100%) application layout. .form-group = f.label :dashboard, class: 'label-light' do - Default Dashboard - = link_to('(?)', help_page_path('profile/preferences') + '#default-dashboard', target: '_blank') + Default dashboard = f.select :dashboard, dashboard_choices, {}, class: 'form-control' .form-group = f.label :project_view, class: 'label-light' do Project view - = link_to('(?)', help_page_path('profile/preferences') + '#default-project-view', target: '_blank') = f.select :project_view, project_view_choices, {}, class: 'form-control' .help-block Choose what content you want to see on a project's home page. diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 79a0dc1b959..0fd19780570 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,6 +1,6 @@ - empty_repo = @project.empty_repo? .project-home-panel.text-center{ class: ("empty-project" if empty_repo) } - %div{ class: container_class } + .limit-container-width{ class: container_class } .avatar-container.s70.project-avatar = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile') %h1.project-title diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index edf55d59f28..de8c173f26f 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -3,7 +3,7 @@ .top-block.row-content-block.clearfix .pull-right = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), - class: 'btn btn-default download' do + rel: 'nofollow', download: '', class: 'btn btn-default download' do = icon('download') Download artifacts archive diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index e7adef5558a..4b344b2edb9 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -1,29 +1,23 @@ +- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create' + .file-holder.file.append-bottom-default - .js-file-title.file-title.clearfix + .js-file-title.file-title.clearfix{ data: { current_action: action } } .editor-ref = icon('code-fork') = ref %span.editor-file-name - if current_action?(:edit) || current_action?(:update) = text_field_tag 'file_path', (params[:file_path] || @path), - class: 'form-control new-file-path' + class: 'form-control new-file-path js-file-path-name-input' - if current_action?(:new) || current_action?(:create) %span.editor-file-name \/ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", - required: true, class: 'form-control new-file-name' + required: true, class: 'form-control new-file-name js-file-path-name-input' .pull-right.file-buttons - .license-selector.js-license-selector-wrap.hidden - = dropdown_tag("Choose a License template", options: { toggle_class: 'btn js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) - .gitignore-selector.js-gitignore-selector-wrap.hidden - = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) - .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden - = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) - .dockerfile-selector.js-dockerfile-selector-wrap.hidden - = dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) - = button_tag class: 'soft-wrap-toggle btn', type: 'button' do + = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do %span.no-wrap = custom_icon('icon_no_wrap') No wrap @@ -31,7 +25,7 @@ = custom_icon('icon_soft_wrap') Soft wrap .encoding-selector - = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' + = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1' .file-editor.code %pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data] diff --git a/app/views/projects/blob/_notebook.html.haml b/app/views/projects/blob/_notebook.html.haml new file mode 100644 index 00000000000..ab1cf933944 --- /dev/null +++ b/app/views/projects/blob/_notebook.html.haml @@ -0,0 +1,5 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('notebook_viewer') + +.file-content#js-notebook-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } } diff --git a/app/views/projects/blob/_pdf.html.haml b/app/views/projects/blob/_pdf.html.haml new file mode 100644 index 00000000000..58dc88e3bf7 --- /dev/null +++ b/app/views/projects/blob/_pdf.html.haml @@ -0,0 +1,5 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('pdf_viewer') + +.file-content#js-pdf-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } } diff --git a/app/views/projects/blob/_sketch.html.haml b/app/views/projects/blob/_sketch.html.haml new file mode 100644 index 00000000000..dad9369cb2a --- /dev/null +++ b/app/views/projects/blob/_sketch.html.haml @@ -0,0 +1,7 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('sketch_viewer') + +.file-content#js-sketch-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } } + .js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' } + = icon('spinner spin 2x', 'aria-hidden' => 'true'); diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml new file mode 100644 index 00000000000..d52733d2bd6 --- /dev/null +++ b/app/views/projects/blob/_template_selectors.html.haml @@ -0,0 +1,17 @@ +.template-selectors-menu + .templates-selectors-label + Template + .template-selector-dropdowns-wrap + .template-type-selector.js-template-type-selector-wrap.hidden + = dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } ) + .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden + = dropdown_tag("Apply a License template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) + .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden + = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) + .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden + = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) + .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden + = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) + .template-selectors-undo-menu.hidden + %span.text-info Template applied + %button.btn.btn-sm.btn-info Undo diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 4924c73cf8e..e14885f264b 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -5,7 +5,7 @@ %a.close{ href: "#", "data-dismiss" => "modal" } ร %h3.page-title= title .modal-body - = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do + = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal', data: { method: method } do .dropzone .dropzone-previews.blob-upload-dropzone-previews %p.dz-message.light @@ -24,8 +24,5 @@ .inline.prepend-left-10 = commit_in_fork_help - -:javascript - gl.utils.disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file'); - new BlobFileDropzone($('.js-upload-blob-form'), '#{method}'); - new NewCommitForm($('.js-upload-blob-form')) +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('blob') diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 3bcddcb37f1..4b26f944733 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -2,7 +2,7 @@ - page_title "Edit", @blob.path, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_bundle_tag('blob_edit') + = page_specific_javascript_bundle_tag('blob') = render "projects/commits/head" %div{ class: container_class } @@ -11,12 +11,15 @@ Someone edited the file the same time you did. Please check out = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer' and make sure your changes will not unintentionally remove theirs. - + .editor-title-row + %h3.page-title.blob-edit-page-title + Edit file + = render 'template_selectors' .file-editor %ul.nav-links.no-bottom.js-edit-mode %li.active = link_to '#editor' do - Edit File + Write %li = link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index e0ce8cc9601..2afb909572a 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,11 +1,11 @@ - page_title "New File", @path.presence, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_bundle_tag('blob_edit') - -%h3.page-title - New File - + = page_specific_javascript_bundle_tag('blob') +.editor-title-row + %h3.page-title.blob-new-page-title + New file + = render 'template_selectors' .file-editor = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do = render 'projects/blob/editor', ref: @ref diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index added3f669b..7ca0ec8ed2b 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -6,10 +6,8 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('filtered_search') = page_specific_javascript_bundle_tag('boards') - = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test? %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" - %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list" %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal = render "projects/issues/head" diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index 0bca6a786cb..5a4eaf92b16 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -7,12 +7,12 @@ data: { container: "body", placement: "bottom" } } {{ list.title }} .board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' } - %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' } + %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' } {{ list.issuesSize }} - if can?(current_user, :admin_issue, @project) %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button", "@click" => "showNewIssueForm", - "v-if" => 'list.type !== "done"', + "v-if" => 'list.type !== "closed"', "aria-label" => "Add an issue", "title" => "Add an issue", data: { placement: "top", container: "body" } } diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml deleted file mode 100644 index 4a4dd84d5d2..00000000000 --- a/app/views/projects/boards/components/_board_list.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -.board-list-component - .board-list-loading.text-center{ "v-if" => "loading" } - = icon("spinner spin") - - if can? current_user, :create_issue, @project - %board-new-issue{ ":list" => "list", - "v-if" => 'list.type !== "done" && showIssueForm' } - %ul.board-list{ "ref" => "list", - "v-show" => "!loading", - ":data-board" => "list.id", - ":class" => '{ "is-smaller": showIssueForm }' } - %board-card{ "v-for" => "(issue, index) in issues", - "ref" => "issue", - ":index" => "index", - ":list" => "list", - ":issue" => "issue", - ":issue-link-base" => "issueLinkBase", - ":root-path" => "rootPath", - ":disabled" => "disabled", - ":key" => "issue.id" } - %li.board-list-count.text-center{ "v-if" => "showCount", - "data-issue-id" => "-1" } - = icon("spinner spin", "v-show" => "list.loadingMore" ) - %span{ "v-if" => "list.issues.length === list.issuesSize" } - Showing all issues - %span{ "v-else" => true } - Showing {{ list.issues.length }} of {{ list.issuesSize }} issues diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 78720d88e4e..6f45d5b0689 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -1,6 +1,6 @@ - builds = @build.pipeline.builds.to_a -%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "151", "spy" => "affix" } } +%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "153", "spy" => "affix" } } .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default Job %strong ##{@build.id} @@ -33,7 +33,7 @@ = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do Keep - = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do + = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do Download - if @build.artifacts_metadata? @@ -137,3 +137,6 @@ = build.id - if build.retried? %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } + +:javascript + new Sidebar(); diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 307010edb58..d5fe771613c 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,6 +1,6 @@ - @no_container = true - page_title "#{@build.name} (##{@build.id})", "Jobs" -= render "projects/pipelines/head", build_subnav: true += render "projects/pipelines/head" %div{ class: container_class } .build-page diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 09286a1b3c6..aeed293a724 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -94,7 +94,7 @@ %td .pull-right - if can?(current_user, :read_build, build) && build.artifacts? - = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do + = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do = icon('download') - if can?(current_user, :update_build, build) - if build.active? diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index da5a676274f..09e3a775d1c 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -1,6 +1,7 @@ - disable_initialization = local_assigns.fetch(:disable_initialization, false) #commit-pipeline-table-view{ data: { disable_initialization: disable_initialization, endpoint: endpoint, + "help-page-path" => help_page_path('ci/quick_start/README'), } } - content_for :page_specific_javascripts do diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 6ab9a80e083..4b1ff75541a 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -24,7 +24,7 @@ .visible-xs-inline = render_commit_status(commit, ref: ref) - if commit.description? - %a.text-expander.hidden-xs.js-toggle-button ... + %button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ... - if commit.description? %pre.commit-row-description.js-toggle-content diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 8e24e28765f..fd4f3c8d3cc 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -1,7 +1,7 @@ .js-toggle-container .commit-stat-summary Showing - = link_to '#', class: 'js-toggle-button' do + %button.diff-stats-summary-toggler.js-toggle-button{ type: "button" } %strong= pluralize(diff_files.size, "changed file") with %strong.cgreen #{diff_files.sum(&:added_lines)} additions diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 2802a4eca7b..160345cfaa5 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -31,7 +31,7 @@ = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) .form-group = f.label :tag_list, "Tags", class: 'label-light' - = f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control" + = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" %p.help-block Separate tags with commas. %hr %fieldset @@ -163,7 +163,7 @@ - if @project.export_project_path = link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project), - method: :get, class: "btn btn-default" + rel: 'nofollow', download: '', method: :get, class: "btn btn-default" = link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project), method: :post, class: "btn btn-default" - else @@ -238,6 +238,8 @@ %ul %li Be careful. Renaming a project's repository can have unintended side effects. %li You will need to update your local repositories to point to the new location. + - if @project.deployment_services.any? + %li Your deployment services will be broken, you will need to manually fix the services after renaming. = f.submit 'Rename project', class: "btn btn-warning" - if can?(current_user, :change_namespace, @project) %hr diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml index bf0f1819073..a82ef5ee5bb 100644 --- a/app/views/projects/environments/_external_url.html.haml +++ b/app/views/projects/environments/_external_url.html.haml @@ -1,3 +1,4 @@ - if environment.external_url && can?(current_user, :read_environment, environment) = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do = icon('external-link') + View deployment diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml index acbac1869fd..e27281d6917 100644 --- a/app/views/projects/environments/_metrics_button.html.haml +++ b/app/views/projects/environments/_metrics_button.html.haml @@ -4,3 +4,4 @@ = link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do = icon('area-chart') + Monitoring diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index b8c1782f050..92dc58cd38d 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -11,14 +11,18 @@ .col-sm-6 %h3.page-title Environment: - = @environment.name + = link_to @environment.name, environment_path(@environment) .col-sm-6 .nav-controls = render 'projects/deployments/actions', deployment: @environment.last_deployment .row .col-sm-12 + %h4 + CPU utilization %svg.prometheus-graph{ 'graph-type' => 'cpu_values' } .row .col-sm-12 + %h4 + Memory usage %svg.prometheus-graph{ 'graph-type' => 'memory_values' } diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index f463a429f65..ff6aaebda22 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -4,9 +4,9 @@ %div{ class: container_class } .top-area.adjust - .col-md-9 + .col-md-7 %h3.page-title= @environment.name - .col-md-3 + .col-md-5 .nav-controls = render 'projects/environments/metrics_button', environment: @environment = render 'projects/environments/terminal_button', environment: @environment diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index ef0dd0eda3c..c8363087d6a 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -16,7 +16,7 @@ .col-sm-6 .nav-controls - = link_to @environment.external_url, class: 'btn btn-default' do + = link_to @environment.external_url, class: 'btn btn-default', target: '_blank', rel: 'noopener noreferrer nofollow' do = icon('external-link') = render 'projects/deployments/actions', deployment: @environment.last_deployment diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 6ac05bf3afe..885795ccb5c 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -49,11 +49,12 @@ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' - .issue-details.issuable-details .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) } - %h2.title - = markdown_field(@issue, :title) + .issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title), + "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue), + } } + .issue-title-entrypoint - if @issue.description.present? .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .wiki @@ -77,3 +78,5 @@ = render 'projects/issues/discussion' = render 'shared/issuable/sidebar', issuable: @issue + += page_specific_javascript_bundle_tag('issue_show') diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 8d4a91cb64c..29f861c09c6 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -3,9 +3,6 @@ - hide_class = '' = render "projects/issues/head" -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test? - - if @labels.exists? || @prioritized_labels.exists? %div{ class: container_class } .top-area.adjust diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml index fe82f751f53..4e97f74dd6a 100644 --- a/app/views/projects/merge_requests/_merge_requests.html.haml +++ b/app/views/projects/merge_requests/_merge_requests.html.haml @@ -1,8 +1,8 @@ %ul.content-list.mr-list.issuable-list - = render @merge_requests - - if @merge_requests.blank? - %li - .nothing-here-block No merge requests to show + - if @merge_requests.exists? + = render @merge_requests + - else + = render 'shared/empty_states/merge_requests' - if @merge_requests.present? = paginate @merge_requests, theme: "gitlab" diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index ad14b4e583e..8d134aaac67 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -21,7 +21,7 @@ selected: f.object.source_project_id .merge-request-select.dropdown = f.hidden_field :source_branch - = dropdown_toggle local_assigns.fetch(f.object.source_branch, "Select source branch"), { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } + = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" } .dropdown-menu.dropdown-menu-selectable.dropdown-source-branch = dropdown_title("Select source branch") = dropdown_filter("Search branches") diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 6682a85ffa6..881ee9fd596 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -52,8 +52,10 @@ = render 'award_emoji/awards_block', awardable: @merge_request, inline: true .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - .merge-request-tabs-container - %ul.merge-request-tabs.nav-links.no-top.no-bottom + .merge-request-tabs-container.scrolling-tabs-container.inner-page-scroll-tabs + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.merge-request-tabs.nav-links.scrolling-tabs %li.notes-tab = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do Discussion diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml index 8a96c8dacf6..64f17ab34b1 100644 --- a/app/views/projects/merge_requests/index.html.haml +++ b/app/views/projects/merge_requests/index.html.haml @@ -7,16 +7,19 @@ - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('filtered_search') -%div{ class: container_class } - .top-area - = render 'shared/issuable/nav', type: :merge_requests - .nav-controls - - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - - if merge_project - = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do - New Merge Request +- if @project.merge_requests.exists? + %div{ class: container_class } + .top-area + = render 'shared/issuable/nav', type: :merge_requests + .nav-controls + - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) + - if merge_project + = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do + New merge request - = render 'shared/issuable/search_bar', type: :merge_requests + = render 'shared/issuable/search_bar', type: :merge_requests - .merge-requests-holder - = render 'merge_requests' + .merge-requests-holder + = render 'merge_requests' +- else + = render 'shared/empty_states/merge_requests', button_path: new_namespace_project_merge_request_path(@project.namespace, @project) diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index f0a23bec5e7..e632fc681cf 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -1,7 +1,8 @@ - case @status - when :success + - remove_source_branch = params[:should_remove_source_branch] == '1' || @merge_request.remove_source_branch? :plain - merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'}); + merge_request_widget.mergeInProgress(#{remove_source_branch}); - when :merge_when_pipeline_succeeds :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}"); diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index c94c7944c0b..e5ec151a61d 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -37,7 +37,7 @@ = check_box_tag :should_remove_source_branch Remove source branch .accept-control - = link_to "#", class: "modify-merge-commit-link js-toggle-button" do + %button.modify-merge-commit-link.js-toggle-button{ type: "button" } = icon('edit') Modify commit message .js-toggle-content.hide.prepend-top-default diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 918f5d161bb..b6340a00b29 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -7,6 +7,7 @@ = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) .nav-controls + = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New Milestone' do New Milestone diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index d16f49bd33a..5249d752585 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -3,9 +3,6 @@ - page_description @milestone.description = render "projects/issues/head" -- content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test? - %div{ class: container_class } .detail-page-header.milestone-page-header .status-box{ class: status_box_class(@milestone) } @@ -36,6 +33,9 @@ = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do Delete + %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" } + = icon('angle-double-left') + .detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) } %h2.title = markdown_field(@milestone, :title) @@ -53,5 +53,5 @@ .alert.alert-success.prepend-top-default %span All issues for this milestone are closed. You may close this milestone now. - = render 'shared/milestones/summary', milestone: @milestone, project: @project = render 'shared/milestones/tabs', milestone: @milestone + = render 'shared/milestones/sidebar', milestone: @milestone, project: @project, affix_offset: 153 diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index d129da943f8..0c7b53e5a9a 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -23,7 +23,7 @@ - if current_user.can_select_namespace? .input-group-addon = root_url - = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true), {}, {class: 'select2 js-select-namespace', tabindex: 1} + = f.select :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), {}, { class: 'select2 js-select-namespace', tabindex: 1} - else .input-group-addon.static-namespace @@ -76,9 +76,9 @@ Gitea %div - if git_import_enabled? - = link_to "#", class: 'btn js-toggle-button import_git' do + %button.btn.js-toggle-button.import_git{ type: "button" } = icon('git', text: 'Repo by URL') - .import_gitlab_project + .import_gitlab_project.has-tooltip{ data: { container: 'body' } } - if gitlab_project_import_enabled? = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do = icon('gitlab', text: 'GitLab export') @@ -109,6 +109,9 @@ %p Please wait a moment, this page will automatically refresh when ready. :javascript + var importBtnTooltip = "Please enter a valid project name."; + var $importBtnWrapper = $('.import_gitlab_project'); + $('.how_to_import_link').bind('click', function (e) { e.preventDefault(); var import_modal = $(this).next(".modal").show(); @@ -123,15 +126,8 @@ $(".btn_import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val()); }); - $('.btn_import_gitlab_project').attr('disabled',true) - $('.import_gitlab_project').attr('title', 'Project path and name required.'); - - $('.import_gitlab_project').click(function( event ) { - if($('.btn_import_gitlab_project').attr('disabled')) { - event.preventDefault(); - new Flash("Please enter path and name for the project to be imported to."); - } - }); + $('.btn_import_gitlab_project').attr('disabled', $('#project_path').val().trim().length === 0); + $importBtnWrapper.attr('title', importBtnTooltip); $('#new_project').submit(function(){ var $path = $('#project_path'); @@ -139,13 +135,13 @@ }); $('#project_path').keyup(function(){ - if($(this).val().length !=0) { + if($(this).val().trim().length !== 0) { $('.btn_import_gitlab_project').attr('disabled', false); - $('.import_gitlab_project').attr('title',''); - $(".flash-container").html("") + $importBtnWrapper.attr('title',''); + $importBtnWrapper.removeClass('has-tooltip'); } else { $('.btn_import_gitlab_project').attr('disabled',true); - $('.import_gitlab_project').attr('title', 'Project path and name required.'); + $importBtnWrapper.addClass('has-tooltip'); } }); diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index 5552086bc50..6c0e6d48d6c 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -37,7 +37,7 @@ ":can-resolve" => can_resolve, ":author-name" => "'#{j(note.author.name)}'", "author-avatar" => note.author.avatar_url, - ":note-truncated" => "'#{truncate(note.note, length: 17)}'", + ":note-truncated" => "'#{j(truncate(note.note, length: 17))}'", ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'", "v-show" => "#{can_resolve || note.resolved?}", "inline-template" => true, diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index a5acb7ac4a5..bc57f7f1c46 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -1,7 +1,7 @@ = content_for :sub_nav do .scrolling-tabs-container.sub-nav-scroll = render 'shared/nav_scroll' - .nav-links.sub-nav.scrolling-tabs{ class: ('build' if local_assigns.fetch(:build_subnav, false)) } + .nav-links.sub-nav.scrolling-tabs %ul{ class: (container_class) } - if project_nav_tab? :pipelines = nav_link(path: 'pipelines#index', controller: :pipelines) do @@ -10,13 +10,13 @@ Pipelines - if project_nav_tab? :builds - = nav_link(path: 'builds#index', controller: :builds) do + = nav_link(controller: :builds) do = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do %span Jobs - if project_nav_tab? :environments - = nav_link(path: 'environments#index', controller: :environments) do + = nav_link(controller: :environments) do = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do %span Environments diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 0605af4fcd3..4be9a1371ec 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -1,10 +1,12 @@ .page-content-header .header-main-content = render 'ci/status/badge', status: @pipeline.detailed_status(current_user) - %strong Pipeline ##{@commit.pipelines.last.id} - triggered #{time_ago_with_tooltip(@commit.authored_date)} by - = author_avatar(@commit, size: 24) - = commit_author_link(@commit) + %strong Pipeline ##{@pipeline.id} + triggered #{time_ago_with_tooltip(@pipeline.created_at)} + - if @pipeline.user + by + = user_avatar(user: @pipeline.user, size: 24) + = user_link(@pipeline.user) .header-action-buttons - if can?(current_user, :update_pipeline, @pipeline.project) - if @pipeline.retryable? diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 5d59ce06612..3d73284699f 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -2,53 +2,19 @@ - page_title "Pipelines" = render "projects/pipelines/head" -%div{ class: container_class } - .top-area - %ul.nav-links - %li.js-pipelines-tab-all{ class: active_when(@scope.nil?) }> - = link_to project_pipelines_path(@project) do - All - %span.badge.js-totalbuilds-count - = number_with_delimiter(@pipelines_count) - - %li.js-pipelines-tab-pending{ class: active_when(@scope == 'pending') }> - = link_to project_pipelines_path(@project, scope: :pending) do - Pending - %span.badge - = number_with_delimiter(@pending_count) - - %li.js-pipelines-tab-running{ class: active_when(@scope == 'running') }> - = link_to project_pipelines_path(@project, scope: :running) do - Running - %span.badge.js-running-count - = number_with_delimiter(@running_count) - - %li.js-pipelines-tab-finished{ class: active_when(@scope == 'finished') }> - = link_to project_pipelines_path(@project, scope: :finished) do - Finished - %span.badge - = number_with_delimiter(@finished_count) - - %li.js-pipelines-tab-branches{ class: active_when(@scope == 'branches') }> - = link_to project_pipelines_path(@project, scope: :branches) do - Branches - - %li.js-pipelines-tab-tags{ class: active_when(@scope == 'tags') }> - = link_to project_pipelines_path(@project, scope: :tags) do - Tags - - .nav-controls - - if can? current_user, :create_pipeline, @project - = link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do - Run pipeline - - - unless @repository.gitlab_ci_yml - = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' - - = link_to ci_lint_path, class: 'btn btn-default' do - %span CI Lint - .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } - .vue-pipelines-index +#pipelines-list-vue{ data: { endpoint: namespace_project_pipelines_path(@project.namespace, @project, format: :json), + "css-class" => container_class, + "help-page-path" => help_page_path('ci/quick_start/README'), + "new-pipeline-path" => new_namespace_project_pipeline_path(@project.namespace, @project), + "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, + "all-path" => project_pipelines_path(@project), + "pending-path" => project_pipelines_path(@project, scope: :pending), + "running-path" => project_pipelines_path(@project, scope: :running), + "finished-path" => project_pipelines_path(@project, scope: :finished), + "branches-path" => project_pipelines_path(@project, scope: :branches), + "tags-path" => project_pipelines_path(@project, scope: :tags), + "has-ci" => @repository.gitlab_ci_yml, + "ci-lint-path" => ci_lint_path } } = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('vue_pipelines') diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml index a9e27df5a87..5af0cc7a2f3 100644 --- a/app/views/projects/protected_branches/_dropdown.html.haml +++ b/app/views/projects/protected_branches/_dropdown.html.haml @@ -10,6 +10,6 @@ %ul.dropdown-footer-list %li - = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do + %button{ class: "create-new-protected-branch-button js-create-new-protected-branch", title: "New Protected Branch" } Create wildcard %code diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml index 964133504e6..86d5a0ec7b8 100644 --- a/app/views/projects/services/_index.html.haml +++ b/app/views/projects/services/_index.html.haml @@ -18,7 +18,7 @@ %th Last edit - @services.sort_by(&:title).each do |service| %tr - %td + %td{ "aria-label" => "#{service.title}: status " + (service.activated? ? "on" : "off") } = boolean_to_icon service.activated? %td = link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index de1229d58aa..edfe6da1816 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -13,7 +13,7 @@ = render "home_panel" - if current_user && can?(current_user, :download_code, @project) - %nav.project-stats{ class: container_class } + %nav.project-stats.limit-container-width{ class: container_class } %ul.nav %li = link_to project_files_path(@project) do @@ -74,11 +74,11 @@ Set up auto deploy - if @repository.commit - %div{ class: container_class } + .limit-container-width{ class: container_class } .project-last-commit = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project -%div{ class: container_class } +.limit-container-width{ class: container_class } - if @project.archived? .text-warning.center.prepend-top-20 %p diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index 9c5eb501174..671a3ef481c 100644 --- a/app/views/projects/stage/_in_stage_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -1,5 +1,5 @@ - group_status = CommitStatus.where(id: subject).status -%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } } +%button.dropdown-menu-toggle.build-content.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}", container: 'body' } } %span{ class: "ci-status-icon ci-status-icon-#{group_status}" } = ci_icon_for_status(group_status) %span.ci-status-text diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index c52527332bc..0d2cd4a7476 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,3 +1,5 @@ +- commit_message = @page.persisted? ? "Update #{@page.title}" : "Create #{@page.title}" + = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| = form_errors(@page) @@ -28,7 +30,7 @@ .form-group = f.label :commit_message, class: 'control-label' - .col-sm-10= f.text_field :message, class: 'form-control', rows: 18 + .col-sm-10= f.text_field :message, class: 'form-control', rows: 18, value: commit_message .form-actions - if @page && @page.persisted? diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 8c582f747b3..713b758727e 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -1,4 +1,4 @@ -%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } +%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "spy" => "affix" } } .block.wiki-sidebar-header.append-bottom-default %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" } = icon('angle-double-right') @@ -19,3 +19,6 @@ More Pages = render 'projects/wikis/new' + +:javascript + new Sidebar(); diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 5afb95ac430..059a0d1ac78 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,71 +1,74 @@ -%ul.nav-links.search-filter - - if @project - %li{ class: active_when(@scope == 'blobs') } - = link_to search_filter_path(scope: 'blobs') do - Code - %span.badge - = @search_results.blobs_count - %li{ class: active_when(@scope == 'issues') } - = link_to search_filter_path(scope: 'issues') do - Issues - %span.badge - = @search_results.issues_count - %li{ class: active_when(@scope == 'merge_requests') } - = link_to search_filter_path(scope: 'merge_requests') do - Merge requests - %span.badge - = @search_results.merge_requests_count - %li{ class: active_when(@scope == 'milestones') } - = link_to search_filter_path(scope: 'milestones') do - Milestones - %span.badge - = @search_results.milestones_count - %li{ class: active_when(@scope == 'notes') } - = link_to search_filter_path(scope: 'notes') do - Comments - %span.badge - = @search_results.notes_count - %li{ class: active_when(@scope == 'wiki_blobs') } - = link_to search_filter_path(scope: 'wiki_blobs') do - Wiki - %span.badge - = @search_results.wiki_blobs_count - %li{ class: active_when(@scope == 'commits') } - = link_to search_filter_path(scope: 'commits') do - Commits - %span.badge - = @search_results.commits_count +.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.nav-links.search-filter.scrolling-tabs + - if @project + %li{ class: active_when(@scope == 'blobs') } + = link_to search_filter_path(scope: 'blobs') do + Code + %span.badge + = @search_results.blobs_count + %li{ class: active_when(@scope == 'issues') } + = link_to search_filter_path(scope: 'issues') do + Issues + %span.badge + = @search_results.issues_count + %li{ class: active_when(@scope == 'merge_requests') } + = link_to search_filter_path(scope: 'merge_requests') do + Merge requests + %span.badge + = @search_results.merge_requests_count + %li{ class: active_when(@scope == 'milestones') } + = link_to search_filter_path(scope: 'milestones') do + Milestones + %span.badge + = @search_results.milestones_count + %li{ class: active_when(@scope == 'notes') } + = link_to search_filter_path(scope: 'notes') do + Comments + %span.badge + = @search_results.notes_count + %li{ class: active_when(@scope == 'wiki_blobs') } + = link_to search_filter_path(scope: 'wiki_blobs') do + Wiki + %span.badge + = @search_results.wiki_blobs_count + %li{ class: active_when(@scope == 'commits') } + = link_to search_filter_path(scope: 'commits') do + Commits + %span.badge + = @search_results.commits_count - - elsif @show_snippets - %li{ class: active_when(@scope == 'snippet_blobs') } - = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do - Snippet Contents - %span.badge - = @search_results.snippet_blobs_count - %li{ class: active_when(@scope == 'snippet_titles') } - = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do - Titles and Filenames - %span.badge - = @search_results.snippet_titles_count + - elsif @show_snippets + %li{ class: active_when(@scope == 'snippet_blobs') } + = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do + Snippet Contents + %span.badge + = @search_results.snippet_blobs_count + %li{ class: active_when(@scope == 'snippet_titles') } + = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do + Titles and Filenames + %span.badge + = @search_results.snippet_titles_count - - else - %li{ class: active_when(@scope == 'projects') } - = link_to search_filter_path(scope: 'projects') do - Projects - %span.badge - = @search_results.projects_count - %li{ class: active_when(@scope == 'issues') } - = link_to search_filter_path(scope: 'issues') do - Issues - %span.badge - = @search_results.issues_count - %li{ class: active_when(@scope == 'merge_requests') } - = link_to search_filter_path(scope: 'merge_requests') do - Merge requests - %span.badge - = @search_results.merge_requests_count - %li{ class: active_when(@scope == 'milestones') } - = link_to search_filter_path(scope: 'milestones') do - Milestones - %span.badge - = @search_results.milestones_count + - else + %li{ class: active_when(@scope == 'projects') } + = link_to search_filter_path(scope: 'projects') do + Projects + %span.badge + = @search_results.projects_count + %li{ class: active_when(@scope == 'issues') } + = link_to search_filter_path(scope: 'issues') do + Issues + %span.badge + = @search_results.issues_count + %li{ class: active_when(@scope == 'merge_requests') } + = link_to search_filter_path(scope: 'merge_requests') do + Merge requests + %span.badge + = @search_results.merge_requests_count + %li{ class: active_when(@scope == 'milestones') } + = link_to search_filter_path(scope: 'milestones') do + Milestones + %span.badge + = @search_results.milestones_count diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index c2d9ac87b20..8869d510aef 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -1,4 +1,6 @@ -- parent = Group.find_by(id: params[:parent_id] || @group.parent_id) +- parent = GroupFinder.new(current_user).execute(id: params[:parent_id] || @group.parent_id) +- group_path = root_url +- group_path << parent.full_path + '/' if parent - if @group.persisted? .form-group = f.label :name, class: 'control-label' do @@ -11,7 +13,7 @@ Group path .col-sm-10 .input-group.gl-field-error-anchor - .input-group-addon + .group-root-path.input-group-addon.has-tooltip{ title: group_path, :'data-placement' => 'bottom' } %span>= root_url - if parent %strong= parent.full_path + '/' diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml index b7982b7fe9b..eecbb32e90e 100644 --- a/app/views/shared/_merge_requests.html.haml +++ b/app/views/shared/_merge_requests.html.haml @@ -6,4 +6,4 @@ = paginate @merge_requests, theme: "gitlab" - else - .nothing-here-block No merge requests to show + = render 'shared/empty_states/merge_requests' diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 57a0eaa919e..db2ac1e1d12 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -4,10 +4,10 @@ Open %span.badge= counts[:opened] %li{ class: milestone_class_for_state(params[:state], 'closed') }> - = link_to milestones_filter_path(state: 'closed') do + = link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc') do Closed %span.badge= counts[:closed] %li{ class: milestone_class_for_state(params[:state], 'all') }> - = link_to milestones_filter_path(state: 'all') do + = link_to milestones_filter_path(state: 'all', sort: 'due_date_desc') do All %span.badge= counts[:all] diff --git a/app/views/shared/_milestones_sort_dropdown.html.haml b/app/views/shared/_milestones_sort_dropdown.html.haml new file mode 100644 index 00000000000..9b2f2fdcc93 --- /dev/null +++ b/app/views/shared/_milestones_sort_dropdown.html.haml @@ -0,0 +1,22 @@ +.dropdown.inline.prepend-left-10 + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } } + %span.light + - if @sort.present? + = milestone_sort_options_hash[@sort] + - else + = sort_title_due_date_soon + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort + %li + = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do + = sort_title_due_date_soon + = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do + = sort_title_due_date_later + = link_to page_filter_path(sort: sort_value_start_date_soon, label: true) do + = sort_title_start_date_soon + = link_to page_filter_path(sort: sort_value_start_date_later, label: true) do + = sort_title_start_date_later + = link_to page_filter_path(sort: sort_value_name, label: true) do + = sort_title_name_asc + = link_to page_filter_path(sort: sort_value_name_desc, label: true) do + = sort_title_name_desc diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml new file mode 100644 index 00000000000..8f1293adcb1 --- /dev/null +++ b/app/views/shared/_user_callout.html.haml @@ -0,0 +1,14 @@ +.user-callout + .bordered-box.landing.content-block + %button.btn.btn-default.close.js-close-callout{ type: 'button', + 'aria-label' => 'Dismiss customize experience box' } + = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true') + .row + .col-sm-3.col-xs-12.svg-container + = custom_icon('icon_customization') + .col-sm-8.col-xs-12.inner-content + %h4 + Customize your experience + %p + Change syntax themes, default project pages, and more in preferences. + = link_to 'Check it out', profile_preferences_path, class: 'btn btn-default js-close-callout' diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml new file mode 100644 index 00000000000..7f2f99f3406 --- /dev/null +++ b/app/views/shared/empty_states/_merge_requests.html.haml @@ -0,0 +1,22 @@ +- button_path = local_assigns.fetch(:button_path, false) +- project_select_button = local_assigns.fetch(:project_select_button, false) +- has_button = button_path || project_select_button + +.row.empty-state.merge-requests + .col-xs-12{ class: "#{'col-sm-6 pull-right' if has_button}" } + .svg-content + = render 'shared/empty_states/icons/merge_requests.svg' + .col-xs-12{ class: "#{'col-sm-6' if has_button}" } + .text-content + - if has_button + %h4 + Merge requests are a place to propose changes you've made to a project and discuss those changes with others. + %p + Interested parties can even contribute by pushing commits if they want to. + - if project_select_button + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: 'New merge request' + - else + = link_to 'New merge request', button_path, class: 'btn btn-new', title: 'New merge request', id: 'new_merge_request_link' + - else + %h4.text-center + There are no merge requests to show. diff --git a/app/views/shared/empty_states/icons/_merge_requests.svg b/app/views/shared/empty_states/icons/_merge_requests.svg new file mode 100644 index 00000000000..e77f6319a95 --- /dev/null +++ b/app/views/shared/empty_states/icons/_merge_requests.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="755 221 385 225" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="278" height="179" rx="10"/><mask id="d" width="278" height="179" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="e" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="f" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g fill="#F9F9F9" transform="translate(752 227)"><rect width="120" height="22" x="30" rx="11"/><rect width="132" height="22" y="44" rx="11"/><rect width="190" height="22" x="208" y="66" rx="11"/><rect width="158" height="22" x="129" y="197" rx="11"/><rect width="158" height="22" x="66" y="154" rx="11"/><rect width="350" height="22" x="31" y="110" rx="11"/><path d="M153 22H21h21.5c6 0 11 5 11 11s-5 11-11 11H21h132-36.5c-6 0-11-5-11-11s5-11 11-11H153zm252 66H288h36.5c6 0 11 5 11 11s-5 11-11 11H288h117-36.5c-6 0-11-5-11-11s5-11 11-11H405zm-244 44H44h36.5c6 0 11 5 11 11s-5 11-11 11H44h117-36.5c-6 0-11-5-11-11s5-11 11-11H161zm75 44H119h21.5c6 0 11 5 11 11s-5 11-11 11H119h117-51.5c-6 0-11-5-11-11s5-11 11-11H236z"/></g><g transform="translate(812 240)"><use fill="#FFF" stroke="#EEE" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#EEE" d="M4 29h271v4H4z"/><g transform="translate(34 60)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 93)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#FC6D26" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#FC6D26" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 126)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(157 59)"><rect width="6" height="2" y="1" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="23" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="34" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="33" fill="#EEE" rx="2"/><rect width="15" height="4" x="58" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="55" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="29" y="44" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" y="33" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="15" y="55" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" y="33" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="48" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="62" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="77" y="22" fill="#EEE" rx="2"/><rect width="6" height="2" y="45" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="56" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="67" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="66" fill="#6B4FBB" rx="2"/><rect width="15" height="4" x="39" y="88" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="77" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="88" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="77" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="34" y="66" fill="#EEE" rx="2"/><rect width="10" height="4" x="72" y="77" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="77" fill="#EEE" rx="2"/><rect width="6" height="2" y="78" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="89" fill="#FDE5D8" rx="1"/></g></g><g transform="translate(1057 221)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="8" mask="url(#e)" xlink:href="#b"/><rect width="29" height="3" x="14" y="14" fill="#FDB692" rx="1.5"/><rect width="39" height="3" x="14" y="23" fill="#FDB692" rx="1.5"/><rect width="29" height="3" x="14" y="32" fill="#FDB692" rx="1.5"/></g><g transform="translate(1046 285)"><circle cx="16" cy="15" r="15" fill="#FFF7F4" stroke="#FC6D26" stroke-width="3"/><path stroke="#FC6D26" stroke-width="2" d="M0 14h1c5 0 9.2-2.7 11.4-6.7M14 1V0"/><path stroke="#FC6D26" stroke-width="2" d="M7.8 3c3 4.3 7.8 7 13.2 7 3.3 0 6.3-1 9-2.7"/><circle cx="10.5" cy="17.5" r="1.5" fill="#FC6D26"/><circle cx="21.5" cy="17.5" r="1.5" fill="#FC6D26"/></g><g transform="translate(825 370)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M25 7h2.7C25 2.8 20.4 0 15 0 9.6 0 5 2.8 2.3 7H5l2.5-3L10 7l2.5-3L15 7l2.5-3L20 7l2.5-3L25 7z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g><g transform="matrix(-1 0 0 1 840 306)"><use fill="#FFF" stroke="#E2DCF2" stroke-width="8" mask="url(#f)" xlink:href="#c"/><rect width="29" height="3" x="24" y="14" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="23" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="32" fill="#6B4FBB" opacity=".5" rx="1.5"/></g></g></svg> diff --git a/app/views/shared/empty_states/icons/_pipelines_empty.svg b/app/views/shared/empty_states/icons/_pipelines_empty.svg new file mode 100644 index 00000000000..8119d5bebe0 --- /dev/null +++ b/app/views/shared/empty_states/icons/_pipelines_empty.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 150"><g fill="none" fill-rule="evenodd" transform="translate(0-3)"><g transform="translate(0 105)"><g fill="#e5e5e5"><rect width="78" height="4" x="34" y="21" opacity=".5" rx="2"/><path d="m152 23c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2m14 0c0-1.105.887-2 1.998-2h4c1.104 0 1.998.888 1.998 2 0 1.105-.887 2-1.998 2h-4c-1.104 0-1.998-.888-1.998-2"/></g><g transform="translate(0 4)"><path fill="#98d7b2" fill-rule="nonzero" d="m19 38c-10.493 0-19-8.507-19-19 0-10.493 8.507-19 19-19 10.493 0 19 8.507 19 19 0 10.493-8.507 19-19 19m0-4c8.284 0 15-6.716 15-15 0-8.284-6.716-15-15-15-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15"/><path fill="#31af64" d="m17.07 21.02l-2.829-2.829c-.786-.786-2.047-.781-2.828 0-.786.786-.781 2.047 0 2.828l4.243 4.243c.392.392.902.587 1.412.588.512.002 1.021-.193 1.41-.582l7.79-7.79c.777-.777.775-2.042-.006-2.823-.786-.786-2.045-.784-2.823-.006l-6.37 6.37"/></g><g fill="#e52c5a" transform="translate(102)"><path fill-rule="nonzero" d="m24 47.5c-12.979 0-23.5-10.521-23.5-23.5 0-12.979 10.521-23.5 23.5-23.5 12.979 0 23.5 10.521 23.5 23.5 0 12.979-10.521 23.5-23.5 23.5m0-5c10.217 0 18.5-8.283 18.5-18.5 0-10.217-8.283-18.5-18.5-18.5-10.217 0-18.5 8.283-18.5 18.5 0 10.217 8.283 18.5 18.5 18.5"/><path d="m28.24 24l2.833-2.833c1.167-1.167 1.167-3.067-.004-4.239-1.169-1.169-3.069-1.173-4.239-.004l-2.833 2.833-2.833-2.833c-1.167-1.167-3.067-1.167-4.239.004-1.169 1.169-1.173 3.069-.004 4.239l2.833 2.833-2.833 2.833c-1.167 1.167-1.167 3.067.004 4.239 1.169 1.169 3.069 1.173 4.239.004l2.833-2.833 2.833 2.833c1.167 1.167 3.067 1.167 4.239-.004 1.169-1.169 1.173-3.069.004-4.239l-2.833-2.833"/></g><path fill="#e5e5e5" fill-rule="nonzero" d="m236 37c-7.732 0-14-6.268-14-14 0-7.732 6.268-14 14-14 7.732 0 14 6.268 14 14 0 7.732-6.268 14-14 14m0-4c5.523 0 10-4.477 10-10 0-5.523-4.477-10-10-10-5.523 0-10 4.477-10 10 0 5.523 4.477 10 10 10"/></g><g transform="translate(69 3)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 11.99v60.02c0 4.413 3.583 7.99 8 7.99h89.991c4.419 0 8-3.579 8-7.99v-60.02c0-4.413-3.583-7.99-8-7.99h-89.991c-4.419 0-8 3.579-8 7.99m-4 0c0-6.622 5.378-11.99 12-11.99h89.991c6.629 0 12 5.367 12 11.99v60.02c0 6.622-5.378 11.99-12 11.99h-89.991c-6.629 0-12-5.367-12-11.99v-60.02m52.874 80.3l-13.253-15.292h34.76l-13.253 15.292c-2.237 2.582-6.01 2.585-8.253 0m3.02-2.62c.644.743 1.564.743 2.207 0l7.516-8.673h-17.24l7.516 8.673"/><rect width="18" height="6" x="15" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="39" y="39" fill="#e52c5a" rx="3"/><rect width="18" height="6" x="33" y="55" fill="#e5e5e5" rx="3"/><rect width="12" height="6" x="39" y="23" fill="#fde5d8" rx="3"/><rect width="12" height="6" x="57" y="55" fill="#e52c5a" rx="3"/><rect width="12" height="6" x="15" y="55" fill="#b5a7dd" rx="3"/><rect width="18" height="6" x="81" y="23" fill="#fc8a51" rx="3"/><rect width="18" height="6" x="15" y="39" fill="#fde5d8" rx="3"/><rect width="6" height="6" x="57" y="23" fill="#e52c5a" rx="3"/><g fill="#fde5d8"><rect width="6" height="6" x="69" y="23" rx="3"/><rect width="6" height="6" x="75" y="39" rx="3"/></g><rect width="6" height="6" x="63" y="39" fill="#e52c5a" rx="3"/></g><g transform="matrix(.70711-.70711.70711.70711 84.34 52.5)"><path fill="#6b4fbb" fill-rule="nonzero" d="m28.02 67.48c-15.927-2.825-28.02-16.738-28.02-33.476 0-18.778 15.222-34 34-34 18.778 0 34 15.222 34 34 0 16.738-12.1 30.652-28.02 33.476.015.173.023.347.023.524v21.999c0 3.314-2.693 6-6 6-3.314 0-6-2.682-6-6v-21.999c0-.177.008-.351.023-.524m5.977-7.476c14.359 0 26-11.641 26-26 0-14.359-11.641-26-26-26-14.359 0-26 11.641-26 26 0 14.359 11.641 26 26 26"/><path fill="#fff" fill-opacity=".3" stroke="#6b4fbb" stroke-width="8" d="m31 71c16.569 0 30-13.431 30-30 0-16.569-13.431-30-30-30" transform="matrix(.86603.5-.5.86603 26.663-17.507)"/></g></g></svg>
\ No newline at end of file diff --git a/app/views/shared/empty_states/icons/_pipelines_failed.svg b/app/views/shared/empty_states/icons/_pipelines_failed.svg new file mode 100644 index 00000000000..7dbabf7e4ef --- /dev/null +++ b/app/views/shared/empty_states/icons/_pipelines_failed.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 446 249" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="0" d="m260.03 114h23.972v-.013c19.972-.53 36-16.887 36-36.987 0-20.435-16.565-37-37-37-.993 0-1.977.039-2.95.116-4.95-14.605-18.773-25.12-35.05-25.12-5.464 0-10.652 1.185-15.32 3.311-6.649-9.841-17.909-16.311-30.68-16.311-20.435 0-37 16.565-37 37 0 .701.019 1.397.058 2.088-16.11 3.999-28.06 18.561-28.06 35.912 0 20.435 16.565 37 37 37 .324 0 .646-.004.968-.012"/><ellipse id="2" cx="41" cy="41" rx="41" ry="41"/><mask id="1" width="186" height="112" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="82" height="82" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="matrix(.86603.5-.5.86603 228.11 137.43)"><path stroke="#b5a7dd" stroke-width="4" d="m.445.161c15.89 10.636 34.998 16.839 55.55 16.839"/><g transform="translate(56 4)"><path fill="#fb722e" d="m16 8c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2m0 10c0-1.105.902-2 2.01-2h7.983c1.109 0 2.01.888 2.01 2 0 1.105-.902 2-2.01 2h-7.983c-1.109 0-2.01-.888-2.01-2"/><path fill="#fde5d8" fill-rule="nonzero" d="m4 22h6c3.315 0 6-2.685 6-5.997v-6.01c0-3.315-2.684-5.997-6-5.997h-6v18m-4-18.992c0-1.661 1.343-3.01 2.994-3.01h7.01c5.523 0 10 4.47 10 9.997v6.01c0 5.521-4.476 9.997-10 9.997h-7.01c-1.654 0-2.994-1.343-2.994-3.01v-19.984"/></g></g><g fill-rule="nonzero" transform="translate(257)"><path fill="#e5e5e5" d="m3.597 18.747c5.611-9.09 15.519-14.747 26.403-14.747 17.12 0 31 13.879 31 31 0 7.02-2.34 13.685-6.58 19.1l3.149 2.466c4.786-6.111 7.431-13.639 7.431-21.565 0-19.33-15.67-35-35-35-12.286 0-23.476 6.384-29.808 16.647l3.404 2.1"/><g transform="matrix(.96593.25882-.25882.96593 15.98 9.578)"><path fill="#b5a7dd" d="m12.426 11.592l-2.142 1.768-3.664-2.116c-.186-.107-.43-.042-.543.154l-1.229 2.129c-.116.2-.052.438.138.547l3.658 2.112-.455 2.735c-.109.657-.165 1.327-.165 2.01 0 .678.055 1.348.165 2.01l.455 2.735-3.658 2.112c-.186.107-.251.351-.138.547l1.229 2.129c.116.2.353.264.543.154l3.664-2.116 2.142 1.768c1.036.855 2.205 1.533 3.462 2l2.6.972v4.225c0 .215.179.393.405.393h2.458c.231 0 .405-.174.405-.393v-4.225l2.6-.972c1.257-.47 2.426-1.147 3.462-2l2.142-1.768 3.664 2.116c.186.107.43.042.543-.154l1.229-2.129c.116-.2.052-.438-.138-.547l-3.658-2.112.455-2.735c.109-.657.165-1.327.165-2.01 0-.678-.055-1.348-.165-2.01l-.455-2.735 3.658-2.112c.186-.107.251-.351.138-.547l-1.229-2.129c-.116-.2-.353-.264-.543-.154l-3.664 2.116-2.142-1.768c-1.036-.855-2.205-1.533-3.462-2l-2.6-.972v-4.225c0-.215-.179-.393-.405-.393h-2.458c-.231 0-.405.174-.405.393v4.225l-2.6.972c-1.257.47-2.426 1.147-3.462 2m2.062-5.749v-1.45c0-2.426 1.963-4.393 4.405-4.393h2.458c2.433 0 4.405 1.967 4.405 4.393v1.45c1.689.631 3.243 1.538 4.608 2.665l1.259-.727c2.101-1.213 4.786-.497 6.01 1.618l1.229 2.129c1.216 2.107.499 4.798-1.602 6.01l-1.257.726c.144.866.219 1.755.219 2.662 0 .907-.075 1.796-.219 2.662l1.257.726c2.101 1.213 2.823 3.896 1.602 6.01l-1.229 2.129c-1.216 2.107-3.906 2.832-6.01 1.618l-1.259-.727c-1.365 1.127-2.92 2.034-4.608 2.665v1.45c0 2.426-1.963 4.393-4.405 4.393h-2.458c-2.433 0-4.405-1.967-4.405-4.393v-1.45c-1.689-.631-3.243-1.538-4.608-2.665l-1.259.727c-2.101 1.213-4.786.497-6.01-1.618l-1.229-2.129c-1.216-2.107-.499-4.798 1.602-6.01l1.257-.726c-.144-.866-.219-1.755-.219-2.662 0-.907.075-1.796.219-2.662l-1.257-.726c-2.101-1.213-2.823-3.896-1.602-6.01l1.229-2.129c1.216-2.107 3.906-2.832 6.01-1.618l1.259.727c1.365-1.127 2.92-2.034 4.608-2.665"/><path fill="#6b4fbb" d="m20.12 23.366c1.347 0 2.439-1.092 2.439-2.439 0-1.347-1.092-2.439-2.439-2.439-1.347 0-2.439 1.092-2.439 2.439 0 1.347 1.092 2.439 2.439 2.439m0 4c-3.556 0-6.439-2.883-6.439-6.439 0-3.556 2.883-6.439 6.439-6.439 3.556 0 6.439 2.883 6.439 6.439 0 3.556-2.883 6.439-6.439 6.439"/></g></g><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#1)" stroke-linejoin="round" xlink:href="#0"/><g transform="translate(175 58)"><use fill="#fff" stroke="#e5e5e5" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#e5e5e5" d="m41 78c20.435 0 37-16.565 37-37 0-20.435-16.565-37-37-37-20.435 0-37 16.565-37 37 0 20.435 16.565 37 37 37m0 4c-22.644 0-41-18.356-41-41 0-22.644 18.356-41 41-41 22.644 0 41 18.356 41 41 0 22.644-18.356 41-41 41"/><g transform="matrix(.96593.25882-.25882.96593 23.581 9.415)"><path fill="#b5a7dd" d="m14.821 13.655l-2.142 1.768-3.933-2.271c-.72-.416-1.634-.171-2.046.543l-1.507 2.61c-.409.708-.161 1.631.553 2.043l3.926 2.267-.455 2.735c-.145.869-.218 1.754-.218 2.65 0 .896.073 1.782.218 2.65l.455 2.735-3.926 2.267c-.72.416-.965 1.329-.553 2.043l1.507 2.61c.409.708 1.332.955 2.046.543l3.933-2.271 2.142 1.768c1.369 1.131 2.916 2.027 4.579 2.648l2.6.972v4.534c0 .831.669 1.5 1.493 1.5h3.01c.817 0 1.493-.676 1.493-1.5v-4.534l2.6-.972c1.663-.621 3.21-1.518 4.579-2.648l2.142-1.768 3.933 2.271c.72.416 1.634.171 2.046-.543l1.507-2.61c.409-.708.161-1.631-.553-2.043l-3.926-2.267.455-2.735c.145-.869.218-1.754.218-2.65 0-.896-.073-1.782-.218-2.65l-.455-2.735 3.926-2.267c.72-.416.965-1.329.553-2.043l-1.507-2.61c-.409-.708-1.332-.955-2.046-.543l-3.933 2.271-2.142-1.768c-1.369-1.131-2.916-2.027-4.579-2.648l-2.6-.972v-4.534c0-.831-.669-1.5-1.493-1.5h-3.01c-.817 0-1.493.676-1.493 1.5v4.534l-2.6.972c-1.663.621-3.21 1.518-4.579 2.648m3.179-6.395v-1.759c0-3.038 2.471-5.5 5.493-5.5h3.01c3.034 0 5.493 2.46 5.493 5.5v1.759c2.098.784 4.03 1.91 5.725 3.311l1.528-.882c2.631-1.519 5.999-.61 7.51 2.01l1.507 2.61c1.517 2.627.616 5.987-2.02 7.507l-1.525.881c.179 1.076.272 2.18.272 3.307 0 1.127-.093 2.231-.272 3.307l1.525.881c2.631 1.519 3.528 4.89 2.02 7.507l-1.507 2.61c-1.517 2.627-4.877 3.527-7.51 2.01l-1.528-.882c-1.696 1.401-3.627 2.527-5.725 3.311v1.759c0 3.038-2.471 5.5-5.493 5.5h-3.01c-3.034 0-5.493-2.46-5.493-5.5v-1.759c-2.098-.784-4.03-1.91-5.725-3.311l-1.528.882c-2.631 1.519-5.999.61-7.51-2.01l-1.507-2.61c-1.517-2.627-.616-5.987 2.02-7.507l1.525-.881c-.179-1.076-.272-2.18-.272-3.307 0-1.127.093-2.231.272-3.307l-1.525-.881c-2.631-1.519-3.528-4.89-2.02-7.507l1.507-2.61c1.517-2.627 4.877-3.527 7.51-2.01l1.528.882c1.696-1.401 3.627-2.527 5.725-3.311"/><path fill="#6b4fbb" d="m25 30c2.209 0 4-1.791 4-4 0-2.209-1.791-4-4-4-2.209 0-4 1.791-4 4 0 2.209 1.791 4 4 4m0 4c-4.418 0-8-3.582-8-8 0-4.418 3.582-8 8-8 4.418 0 8 3.582 8 8 0 4.418-3.582 8-8 8"/></g></g></g><g transform="translate(140 161)"><path fill="#e5e5e5" fill-rule="nonzero" d="m4 8.541v30.01c0 2.202 1.793 3.995 4 3.995h20c2.209 0 4-1.789 4-3.995v-30.01c0-2.202-1.793-3.995-4-3.995h-20c-2.209 0-4 1.789-4 3.995m-4 0c0-4.416 3.583-7.995 8-7.995h20c4.416 0 8 3.584 8 7.995v30.01c0 4.416-3.583 7.995-8 7.995h-20c-4.416 0-8-3.584-8-7.995v-30.01"/><g fill="#fb722e"><rect width="4" height="11" x="10" y="18.545" rx="2"/><rect width="4" height="11" x="21" y="18.545" rx="2"/></g></g><path fill="#e5e5e5" fill-rule="nonzero" d="m445.16 245.34c-16.874-11.778-110.62-20.336-222.14-20.336-111.61 0-205.4 8.571-222.18 20.364-.904.635-1.121 1.883-.486 2.786.635.904 1.883 1.121 2.786.486 15.756-11.07 109.46-19.636 219.88-19.636 110.34 0 203.99 8.55 219.85 19.617.906.632 2.153.41 2.785-.495.632-.906.41-2.153-.495-2.785"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index a95020a9be8..09f946f1d88 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -17,7 +17,7 @@ .stats %span = icon('bookmark') - = number_with_delimiter(group.projects.count) + = number_with_delimiter(group.projects.non_archived.count) %span = icon('users') diff --git a/app/views/shared/icons/_activity.svg b/app/views/shared/icons/_activity.svg deleted file mode 100644 index d465504b154..00000000000 --- a/app/views/shared/icons/_activity.svg +++ /dev/null @@ -1,16 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> - <title>path-1</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="_activity" fill="#7E7D7D"> - <g id="Page-1"> - <g id="path-1"> - <path d="M5,0 C4.448,0 4,0.448 4,1 L4,3 L1,3 C0.448,3 0,3.448 0,4 L0,9 C0,9.552 0.448,10 1,10 L5,10 L5,8 L11,8 L11,10 L15,10 C15.552,10 16,9.552 16,9 L16,4 C16,3.448 15.552,3 15,3 L12,3 L12,1 C12,0.448 11.552,0 11,0 L5,0 L5,0 L5,0 L5,0 Z M6,2.5 C6,2.224 6.224,2 6.5,2 L9.5,2 C9.776,2 10,2.224 10,2.5 C10,2.776 9.776,3 9.5,3 L6.5,3 C6.224,3 6,2.776 6,2.5 L6,2.5 L6,2.5 L6,2.5 Z M6,11 L10.001,11 L10.001,9 L6,9 L6,11 L6,11 L6,11 L6,11 Z M11,11 L11,12 L5,12 L5,11 L1,11 C0.448,11 0,11.448 0,12 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,12 C16,11.448 15.552,11 15,11 L11,11 L11,11 L11,11 L11,11 Z"></path> - </g> - </g> - </g> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_commits.svg b/app/views/shared/icons/_commits.svg deleted file mode 100644 index ba9bb89935e..00000000000 --- a/app/views/shared/icons/_commits.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> - <title>Pasted Image 240</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <path d="M3,8 C3,5.951 4.236,4.194 6,3.422 L6,0 L1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L6,16 L6,12.578 C4.236,11.806 3,10.049 3,8 M7,12.899 L7,16 L9,16 L9,12.899 C8.677,12.965 8.343,13 8,13 C7.657,13 7.323,12.965 7,12.899 M15,0 L10,0 L10,3.422 C11.764,4.194 13,5.951 13,8 C13,10.049 11.764,11.806 10,12.578 L10,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 M10,8 C10,9.105 9.105,10 8,10 C6.895,10 6,9.105 6,8 C6,6.895 6.895,6 8,6 C9.105,6 10,6.895 10,8 M4,8 C4,10.209 5.791,12 8,12 C10.209,12 12,10.209 12,8 C12,5.791 10.209,4 8,4 C5.791,4 4,5.791 4,8 M9,3.101 L9,0 L7,0 L7,3.101 C7.323,3.035 7.657,3 8,3 C8.343,3 8.677,3.035 9,3.101" id="Pasted-Image-240" fill="#7E7D7D"></path> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_contributionanalytics.svg b/app/views/shared/icons/_contributionanalytics.svg deleted file mode 100644 index adf09a14964..00000000000 --- a/app/views/shared/icons/_contributionanalytics.svg +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> - <title>Group</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="Group"> - <path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1" fill="#7E7C7C"></path> - <polygon id="Stroke-6" fill="#7E7C7C" points="2.0197351 9.86809696 6.4567351 6.52409696 5.79233671 6.46815759 9.53233671 10.4271576 9.87070552 10.78534 10.2338016 10.4522494 15.0258016 6.05624938 14.3497984 5.31935062 9.55779844 9.71535062 10.2592633 9.74044241 6.51926329 5.78144241 6.21208651 5.45627854 5.8548649 5.72550304 1.4178649 9.06950304"></polygon> - <path d="M7.0313,6.3928 C7.0313,6.9448 6.5833,7.3928 6.0313,7.3928 C5.4793,7.3928 5.0313,6.9448 5.0313,6.3928 C5.0313,5.8408 5.4793,5.3928 6.0313,5.3928 C6.5833,5.3928 7.0313,5.8408 7.0313,6.3928" id="Fill-8" fill="#FEFEFE"></path> - <path d="M6.5313,6.3928 C6.5313,6.66865763 6.30715763,6.8928 6.0313,6.8928 C5.75544237,6.8928 5.5313,6.66865763 5.5313,6.3928 C5.5313,6.11694237 5.75544237,5.8928 6.0313,5.8928 C6.30715763,5.8928 6.5313,6.11694237 6.5313,6.3928 L6.5313,6.3928 Z M7.5313,6.3928 C7.5313,5.56465763 6.85944237,4.8928 6.0313,4.8928 C5.20315763,4.8928 4.5313,5.56465763 4.5313,6.3928 C4.5313,7.22094237 5.20315763,7.8928 6.0313,7.8928 C6.85944237,7.8928 7.5313,7.22094237 7.5313,6.3928 L7.5313,6.3928 Z" id="Stroke-10" fill="#7E7C7C"></path> - <path d="M10.8854,9.8715 C10.8854,10.4235 10.4374,10.8715 9.8854,10.8715 C9.3334,10.8715 8.8854,10.4235 8.8854,9.8715 C8.8854,9.3195 9.3334,8.8715 9.8854,8.8715 C10.4374,8.8715 10.8854,9.3195 10.8854,9.8715" id="Fill-12" fill="#FEFEFE"></path> - <path d="M10.3854,9.8715 C10.3854,10.1473576 10.1612576,10.3715 9.8854,10.3715 C9.60954237,10.3715 9.3854,10.1473576 9.3854,9.8715 C9.3854,9.59564237 9.60954237,9.3715 9.8854,9.3715 C10.1612576,9.3715 10.3854,9.59564237 10.3854,9.8715 L10.3854,9.8715 Z M11.3854,9.8715 C11.3854,9.04335763 10.7135424,8.3715 9.8854,8.3715 C9.05725763,8.3715 8.3854,9.04335763 8.3854,9.8715 C8.3854,10.6996424 9.05725763,11.3715 9.8854,11.3715 C10.7135424,11.3715 11.3854,10.6996424 11.3854,9.8715 L11.3854,9.8715 Z" id="Stroke-14" fill="#7E7C7C"></path> - </g> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_delta.svg b/app/views/shared/icons/_delta.svg deleted file mode 100644 index 7c0c0d3999c..00000000000 --- a/app/views/shared/icons/_delta.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="14px" height="10px" viewBox="322 21 14 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <path d="M330.078605,22.8166945 L335.259532,29.6235062 C335.615145,30.0907182 335.412062,30.4694683 334.822641,30.4694683 L331.657805,30.4694683 L324.04678,30.4694683 C323.449879,30.4694683 323.260751,30.0822112 323.609889,29.6235062 L328.790816,22.8166945 C329.146429,22.3494825 329.729467,22.3579895 330.078605,22.8166945 Z" id="delta" stroke="#5C5C5C" stroke-width="1" fill="none"></path> -</svg> diff --git a/app/views/shared/icons/_files.svg b/app/views/shared/icons/_files.svg deleted file mode 100644 index fc378d81e40..00000000000 --- a/app/views/shared/icons/_files.svg +++ /dev/null @@ -1,17 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> - <title>Pasted Image 237</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="Pasted-Image-237"> - <path d="M15.1111,16 C15.6021,16 16.0001,15.602 16.0001,15.111 L16.0001,4.444 C15.5341,3.983 12.0671,0.378 11.5551,0 L0.8891,0 C0.3981,0 0.0001,0.398 0.0001,0.889 L0.0001,15.111 C0.0001,15.602 0.3981,16 0.8891,16 L15.1111,16 M14.0001,14.111 L1.8891,14.111 L1.8891,2 L10.8131,2 C11.4451,2.42 13.5811,4.555 14.0001,5.187 L14.0001,14.111" id="Fill-1" fill="#7E7D7D"></path> - <path d="M0.889,0 C0.398,0 0,0.398 0,0.889 L0,15.111 C0,15.602 0.398,16 0.889,16 L15.111,16 C15.602,16 16,15.602 16,15.111 L16,4.445 C15.534,3.983 12.068,0.377 11.555,0 L0.889,0 L0.889,0 Z M1.889,2 L10.813,2 C11.446,2.42 13.581,4.554 14,5.187 L14,14.111 L1.889,14.111 L1.889,2 L1.889,2 Z" id="Clip-4"></path> - <polygon id="Fill-6" fill="#7E7D7D" points="9 7 11 7 11 2 9 2"></polygon> - <polygon id="Clip-9" points="9 7 11 7 11 2.001 9 2.001"></polygon> - <polygon id="Fill-11" fill="#7E7D7D" points="10 7 15.444 7 15.444 5 10 5"></polygon> - <polygon id="Clip-14" points="10 7 15.444 7 15.444 5 10 5"></polygon> - </g> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_icon_close.svg b/app/views/shared/icons/_icon_close.svg index 9d62012518b..59a6cb32d18 100644 --- a/app/views/shared/icons/_icon_close.svg +++ b/app/views/shared/icons/_icon_close.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg> diff --git a/app/views/shared/icons/_icon_empty_groups.svg b/app/views/shared/icons/_icon_empty_groups.svg index 9228be05f03..cf378145e59 100644 --- a/app/views/shared/icons/_icon_empty_groups.svg +++ b/app/views/shared/icons/_icon_empty_groups.svg @@ -1 +1 @@ -<svg width="249" height="368" viewBox="891 156 249 368" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="131" height="162" rx="10"/><mask id="e" x="0" y="0" width="131" height="162" fill="#fff"><use xlink:href="#a"/></mask><path d="M223.616 127.958V108.96c0-4.416-3.584-8-8.005-8h-23.985c-2.778 0-5.98 2.014-7.18 4.5l-5.07 10.5h-49.763c-5.527 0-9.996 4.475-9.996 9.997v53.005c0 5.513 4.475 9.997 9.996 9.997h84.01c5.525 0 9.994-4.477 9.994-9.998v-51.004z" id="b"/><mask id="f" x="0" y="0" width="104" height="88" fill="#fff"><use xlink:href="#b"/></mask><path d="M47 25h.996C53.52 25 58 29.472 58 34.99v20.02C58 60.526 53.52 65 47.996 65H10.004C4.48 65 0 60.528 0 55.01V34.99C0 29.474 4.48 25 10.004 25H11v-7c0-9.94 8.06-18 18-18s18 8.06 18 18v7zm-6 0H17v-7c0-6.627 5.373-12 12-12s12 5.373 12 12v7z" id="c"/><mask id="g" x="0" y="0" width="58" height="65" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 10.008C0 4.48 4.476 0 10 0h218c5.523 0 10 4.473 10 10.008v140.94c0 5.53-4.062 11.882-9.08 14.196l-100.84 46.5c-5.015 2.31-13.142 2.312-18.16 0l-100.84-46.5C4.064 162.832 0 156.484 0 150.95V10.007z" id="d"/><mask id="h" x="0" y="0" width="238" height="213.417" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(891 156)"><g transform="rotate(8 -266.528 490.3)"><use stroke="#E5E5E5" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#a"/><rect fill="#FC8A51" x="20" y="31" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="60" y="31" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="36" y="31" width="20" height="4" rx="2"/><rect fill="#6B4FBB" x="20" y="65" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="44" y="65" width="20" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="80" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="80" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="48" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="60" y="80" width="12" height="4" rx="2"/><rect fill="#6B4FBB" x="52" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="68" y="48" width="12" height="4" rx="2"/></g><use stroke="#B5A7DD" mask="url(#f)" stroke-width="8" fill="#FFF" transform="rotate(5 171.616 144.96)" xlink:href="#b"/><path d="M58 132c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#C1E7D0"/><path d="M90.143 132c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M74.686 133.875l-3.18-3.18c-.29-.29-.77-.296-1.06-.005l-1.55 1.55c-.287.287-.29.766.004 1.06l4.92 4.92c.504.504 1.32.504 1.823 0l.654-.653 7.804-7.804c.3-.3.29-.77-.005-1.067l-1.578-1.58c-.302-.3-.775-.298-1.068-.004l-6.764 6.763z" fill="#31AF64"/><path d="M4 66c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18S4 75.94 4 66z" fill="#D5ECF7"/><path d="M36.143 66c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M22 55.714c5.68 0 10.286 4.605 10.286 10.286 0 5.68-4.605 10.286-10.286 10.286-3.45 0-6.505-1.7-8.37-4.307L22 66V55.714z" fill="#2D9FD8"/><g transform="rotate(-8 748.533 18.147)"><use stroke="#FDE5D8" mask="url(#g)" stroke-width="8" fill="#FFF" xlink:href="#c"/><path d="M31 46.584c1.766-.772 3-2.534 3-4.584 0-2.76-2.24-5-5-5s-5 2.24-5 5c0 2.05 1.234 3.812 3 4.584v3.42c0 1.1.895 1.996 2 1.996 1.112 0 2-.894 2-1.997v-3.42z" fill="#FC8A51"/></g><g transform="translate(0 154)"><use stroke="#E5E5E5" mask="url(#h)" stroke-width="8" fill="#FFF" xlink:href="#d"/><g opacity=".3"><path d="M141.837 104.53l-2.56-7.993-5.074-15.843c-.26-.815-1.398-.815-1.66 0l-5.074 15.843h-16.85l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33 22.16-16.33c.61-.452.866-1.25.632-1.98" fill="#A1A1A1"/><path fill="#5C5C5C" d="M119.044 122.84l8.425-26.303h-16.85l8.424 26.304"/><path fill="#787878" d="M119.044 122.84l-8.425-26.303H98.81l20.232 26.304"/><path fill="#787878" d="M119.044 122.84l8.425-26.303h11.807l-20.233 26.304"/><path d="M98.812 96.537l-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33L98.81 96.538z" fill="#A1A1A1"/><path d="M98.812 96.537h11.807l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843z" fill="#5C5C5C"/><path d="M139.277 96.537l2.56 7.993c.234.73-.022 1.528-.634 1.98l-22.16 16.33 20.234-26.303z" fill="#A1A1A1"/><path d="M139.277 96.537H127.47l5.074-15.843c.26-.815 1.398-.815 1.66 0l5.073 15.843z" fill="#5C5C5C"/></g><path d="M57 18.29c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H41c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H77c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm17 24.693c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm202 32.923c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm202 32.923c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm-202 0c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm202 32.922c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm179.023 19.555c-.988.452-1.388 1.55-.894 2.454.493.904 1.694 1.27 2.682.82l14.31-6.545c.99-.452 1.39-1.55.896-2.454-.494-.902-1.696-1.27-2.684-.817l-14.31 6.544zm-32.2 14.723c-.987.452-1.388 1.55-.894 2.454.493.904 1.695 1.27 2.683.818l14.31-6.544c.99-.45 1.39-1.55.895-2.454-.494-.903-1.695-1.27-2.683-.818l-14.31 6.544zm-32.2 14.724c-.987.45-1.387 1.55-.893 2.454.494.903 1.695 1.27 2.683.818l14.31-6.544c.99-.452 1.39-1.55.896-2.454-.495-.904-1.697-1.27-2.685-.818l-14.31 6.544zm-23.67-2.023l-12.186-5.57c-.987-.452-2.19-.086-2.683.817-.494.904-.093 2.003.895 2.454l12.185 5.573c.754.345 1.57.645 2.438.898 1.052.307 2.177-.224 2.513-1.187.335-.962-.246-1.99-1.298-2.298-.677-.197-1.302-.426-1.864-.684zM62.57 168.437c-.988-.452-2.19-.086-2.683.818-.494.903-.094 2.002.894 2.454l14.31 6.544c.988.45 2.19.085 2.683-.818.494-.904.094-2.003-.894-2.454l-14.312-6.544zm-32.2-14.723c-.988-.452-2.19-.086-2.683.818-.494.904-.093 2.003.895 2.454l14.31 6.544c.988.452 2.19.086 2.684-.818.494-.903.093-2.002-.895-2.454l-14.312-6.543z" fill="#EEE"/></g><g><path d="M104 18c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#FADFD9"/><path d="M136.143 18c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M119.43 8.994c0-.707.57-1.28 1.283-1.28h2.574c.71 0 1.284.57 1.284 1.28v10.298c0 .706-.57 1.28-1.283 1.28h-2.574c-.71 0-1.284-.57-1.284-1.28V8.994zm0 15.433c0-.71.57-1.284 1.283-1.284h2.574c.71 0 1.284.57 1.284 1.284V27c0 .71-.57 1.286-1.283 1.286h-2.574c-.71 0-1.284-.57-1.284-1.285v-2.573z" fill="#E75E40"/></g><g><path d="M213 89c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#F6D4DC"/><path d="M245.143 89c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M231 86.348l-3.603-3.602c-.288-.29-.766-.286-1.063.01l-1.578 1.578c-.3.302-.3.773-.01 1.063L228.348 89l-3.602 3.603c-.29.288-.286.766.01 1.063l1.578 1.578c.302.3.773.3 1.063.01L231 91.652l3.603 3.602c.288.29.766.286 1.063-.01l1.578-1.578c.3-.302.3-.773.01-1.063L233.652 89l3.602-3.603c.29-.288.286-.766-.01-1.063l-1.578-1.578c-.302-.3-.773-.3-1.063-.01L231 86.348z" fill="#D22852"/></g></g></svg>
\ No newline at end of file +<svg width="249" height="368" viewBox="891 156 249 368" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="131" height="162" rx="10"/><mask id="e" x="0" y="0" width="131" height="162" fill="#fff"><use xlink:href="#a"/></mask><path d="M223.616 127.958V108.96c0-4.416-3.584-8-8.005-8h-23.985c-2.778 0-5.98 2.014-7.18 4.5l-5.07 10.5h-49.763c-5.527 0-9.996 4.475-9.996 9.997v53.005c0 5.513 4.475 9.997 9.996 9.997h84.01c5.525 0 9.994-4.477 9.994-9.998v-51.004z" id="b"/><mask id="f" x="0" y="0" width="104" height="88" fill="#fff"><use xlink:href="#b"/></mask><path d="M47 25h.996C53.52 25 58 29.472 58 34.99v20.02C58 60.526 53.52 65 47.996 65H10.004C4.48 65 0 60.528 0 55.01V34.99C0 29.474 4.48 25 10.004 25H11v-7c0-9.94 8.06-18 18-18s18 8.06 18 18v7zm-6 0H17v-7c0-6.627 5.373-12 12-12s12 5.373 12 12v7z" id="c"/><mask id="g" x="0" y="0" width="58" height="65" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 10.008C0 4.48 4.476 0 10 0h218c5.523 0 10 4.473 10 10.008v140.94c0 5.53-4.062 11.882-9.08 14.196l-100.84 46.5c-5.015 2.31-13.142 2.312-18.16 0l-100.84-46.5C4.064 162.832 0 156.484 0 150.95V10.007z" id="d"/><mask id="h" x="0" y="0" width="238" height="213.417" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(891 156)"><g transform="rotate(8 -266.528 490.3)"><use stroke="#E5E5E5" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#a"/><rect fill="#FC8A51" x="20" y="31" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="60" y="31" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="36" y="31" width="20" height="4" rx="2"/><rect fill="#6B4FBB" x="20" y="65" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="44" y="65" width="20" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="80" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="80" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="48" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="60" y="80" width="12" height="4" rx="2"/><rect fill="#6B4FBB" x="52" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="68" y="48" width="12" height="4" rx="2"/></g><use stroke="#B5A7DD" mask="url(#f)" stroke-width="8" fill="#FFF" transform="rotate(5 171.616 144.96)" xlink:href="#b"/><path d="M58 132c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#C1E7D0"/><path d="M90.143 132c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M74.686 133.875l-3.18-3.18c-.29-.29-.77-.296-1.06-.005l-1.55 1.55c-.287.287-.29.766.004 1.06l4.92 4.92c.504.504 1.32.504 1.823 0l.654-.653 7.804-7.804c.3-.3.29-.77-.005-1.067l-1.578-1.58c-.302-.3-.775-.298-1.068-.004l-6.764 6.763z" fill="#31AF64"/><path d="M4 66c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18S4 75.94 4 66z" fill="#D5ECF7"/><path d="M36.143 66c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M22 55.714c5.68 0 10.286 4.605 10.286 10.286 0 5.68-4.605 10.286-10.286 10.286-3.45 0-6.505-1.7-8.37-4.307L22 66V55.714z" fill="#2D9FD8"/><g transform="rotate(-8 748.533 18.147)"><use stroke="#FDE5D8" mask="url(#g)" stroke-width="8" fill="#FFF" xlink:href="#c"/><path d="M31 46.584c1.766-.772 3-2.534 3-4.584 0-2.76-2.24-5-5-5s-5 2.24-5 5c0 2.05 1.234 3.812 3 4.584v3.42c0 1.1.895 1.996 2 1.996 1.112 0 2-.894 2-1.997v-3.42z" fill="#FC8A51"/></g><g transform="translate(0 154)"><use stroke="#E5E5E5" mask="url(#h)" stroke-width="8" fill="#FFF" xlink:href="#d"/><g opacity=".3"><path d="M141.837 104.53l-2.56-7.993-5.074-15.843c-.26-.815-1.398-.815-1.66 0l-5.074 15.843h-16.85l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33 22.16-16.33c.61-.452.866-1.25.632-1.98" fill="#A1A1A1"/><path fill="#5C5C5C" d="M119.044 122.84l8.425-26.303h-16.85l8.424 26.304"/><path fill="#787878" d="M119.044 122.84l-8.425-26.303H98.81l20.232 26.304"/><path fill="#787878" d="M119.044 122.84l8.425-26.303h11.807l-20.233 26.304"/><path d="M98.812 96.537l-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33L98.81 96.538z" fill="#A1A1A1"/><path d="M98.812 96.537h11.807l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843z" fill="#5C5C5C"/><path d="M139.277 96.537l2.56 7.993c.234.73-.022 1.528-.634 1.98l-22.16 16.33 20.234-26.303z" fill="#A1A1A1"/><path d="M139.277 96.537H127.47l5.074-15.843c.26-.815 1.398-.815 1.66 0l5.073 15.843z" fill="#5C5C5C"/></g><path d="M57 18.29c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H41c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H77c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm17 24.693c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm202 32.923c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm202 32.923c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm-202 0c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm202 32.922c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm179.023 19.555c-.988.452-1.388 1.55-.894 2.454.493.904 1.694 1.27 2.682.82l14.31-6.545c.99-.452 1.39-1.55.896-2.454-.494-.902-1.696-1.27-2.684-.817l-14.31 6.544zm-32.2 14.723c-.987.452-1.388 1.55-.894 2.454.493.904 1.695 1.27 2.683.818l14.31-6.544c.99-.45 1.39-1.55.895-2.454-.494-.903-1.695-1.27-2.683-.818l-14.31 6.544zm-32.2 14.724c-.987.45-1.387 1.55-.893 2.454.494.903 1.695 1.27 2.683.818l14.31-6.544c.99-.452 1.39-1.55.896-2.454-.495-.904-1.697-1.27-2.685-.818l-14.31 6.544zm-23.67-2.023l-12.186-5.57c-.987-.452-2.19-.086-2.683.817-.494.904-.093 2.003.895 2.454l12.185 5.573c.754.345 1.57.645 2.438.898 1.052.307 2.177-.224 2.513-1.187.335-.962-.246-1.99-1.298-2.298-.677-.197-1.302-.426-1.864-.684zM62.57 168.437c-.988-.452-2.19-.086-2.683.818-.494.903-.094 2.002.894 2.454l14.31 6.544c.988.45 2.19.085 2.683-.818.494-.904.094-2.003-.894-2.454l-14.312-6.544zm-32.2-14.723c-.988-.452-2.19-.086-2.683.818-.494.904-.093 2.003.895 2.454l14.31 6.544c.988.452 2.19.086 2.684-.818.494-.903.093-2.002-.895-2.454l-14.312-6.543z" fill="#EEE"/></g><g><path d="M104 18c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#FADFD9"/><path d="M136.143 18c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M119.43 8.994c0-.707.57-1.28 1.283-1.28h2.574c.71 0 1.284.57 1.284 1.28v10.298c0 .706-.57 1.28-1.283 1.28h-2.574c-.71 0-1.284-.57-1.284-1.28V8.994zm0 15.433c0-.71.57-1.284 1.283-1.284h2.574c.71 0 1.284.57 1.284 1.284V27c0 .71-.57 1.286-1.283 1.286h-2.574c-.71 0-1.284-.57-1.284-1.285v-2.573z" fill="#E75E40"/></g><g><path d="M213 89c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#F6D4DC"/><path d="M245.143 89c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M231 86.348l-3.603-3.602c-.288-.29-.766-.286-1.063.01l-1.578 1.578c-.3.302-.3.773-.01 1.063L228.348 89l-3.602 3.603c-.29.288-.286.766.01 1.063l1.578 1.578c.302.3.773.3 1.063.01L231 91.652l3.603 3.602c.288.29.766.286 1.063-.01l1.578-1.578c.3-.302.3-.773.01-1.063L233.652 89l3.602-3.603c.29-.288.286-.766-.01-1.063l-1.578-1.578c-.302-.3-.773-.3-1.063-.01L231 86.348z" fill="#D22852"/></g></g></svg> diff --git a/app/views/shared/icons/_icon_mr_issue.svg b/app/views/shared/icons/_icon_mr_issue.svg index ae219a3ded2..a56af9c556c 100644 --- a/app/views/shared/icons/_icon_mr_issue.svg +++ b/app/views/shared/icons/_icon_mr_issue.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="m8.411 1.012c-.136-.008-.273-.012-.411-.012-3.866 0-7 3.134-7 7 0 3.866 3.134 7 7 7 3.866 0 7-3.134 7-7 0-.138-.004-.275-.012-.411-.464.201-.964.334-1.488.386 0 .008 0 .016 0 .025 0 3.038-2.462 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.038 2.462-5.5 5.5-5.5.008 0 .016 0 .025 0 .052-.524.185-1.024.386-1.488"/><path d="m12 2h-1.01c-.54 0-.991.448-.991 1 0 .556.444 1 .991 1h1.01v1.01c0 .54.448.991 1 .991.556 0 1-.444 1-.991v-1.01h1.01c.54 0 .991-.448.991-1 0-.556-.444-1-.991-1h-1.01v-1.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v1.01m-5 4.01c0-.557.444-1.01 1-1.01.552 0 1 .443 1 1.01v1.981c0 .557-.444 1.01-1 1.01-.552 0-1-.443-1-1.01v-1.981m1 5.991c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1"/></g></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="m8.411 1.012c-.136-.008-.273-.012-.411-.012-3.866 0-7 3.134-7 7 0 3.866 3.134 7 7 7 3.866 0 7-3.134 7-7 0-.138-.004-.275-.012-.411-.464.201-.964.334-1.488.386 0 .008 0 .016 0 .025 0 3.038-2.462 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.038 2.462-5.5 5.5-5.5.008 0 .016 0 .025 0 .052-.524.185-1.024.386-1.488"/><path d="m12 2h-1.01c-.54 0-.991.448-.991 1 0 .556.444 1 .991 1h1.01v1.01c0 .54.448.991 1 .991.556 0 1-.444 1-.991v-1.01h1.01c.54 0 .991-.448.991-1 0-.556-.444-1-.991-1h-1.01v-1.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v1.01m-5 4.01c0-.557.444-1.01 1-1.01.552 0 1 .443 1 1.01v1.981c0 .557-.444 1.01-1 1.01-.552 0-1-.443-1-1.01v-1.981m1 5.991c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1"/></g></svg> diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg index e965afa9a56..4c69fc99a9e 100644 --- a/app/views/shared/icons/_icon_play.svg +++ b/app/views/shared/icons/_icon_play.svg @@ -1,3 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play"> - <path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/> - </svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg> diff --git a/app/views/shared/icons/_icon_stopwatch.svg b/app/views/shared/icons/_icon_stopwatch.svg index f20de04538e..6c2a8b2773f 100644 --- a/app/views/shared/icons/_icon_stopwatch.svg +++ b/app/views/shared/icons/_icon_stopwatch.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg> diff --git a/app/views/shared/icons/_icon_timer.svg b/app/views/shared/icons/_icon_timer.svg index 0b1e5804427..572a31ebcca 100644 --- a/app/views/shared/icons/_icon_timer.svg +++ b/app/views/shared/icons/_icon_timer.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><g fill="#8F8F8F" fill-rule="evenodd"><path d="M29.513 10.134A15.922 15.922 0 0 0 23 7.28V6h2.993C26.55 6 27 5.552 27 5V2a1 1 0 0 0-1.007-1H14.007C13.45 1 13 1.448 13 2v3a1 1 0 0 0 1.007 1H17v1.28C9.597 8.686 4 15.19 4 23c0 8.837 7.163 16 16 16s16-7.163 16-16c0-3.461-1.099-6.665-2.967-9.283l1.327-1.58a2.498 2.498 0 0 0-.303-3.53 2.499 2.499 0 0 0-3.528.315l-1.016 1.212zM20 34c6.075 0 11-4.925 11-11s-4.925-11-11-11S9 16.925 9 23s4.925 11 11 11z"/><path d="M19 21h-4.002c-.552 0-.998.452-.998 1.01v1.98c0 .567.447 1.01.998 1.01h7.004c.274 0 .521-.111.701-.291a.979.979 0 0 0 .297-.704v-8.01c0-.54-.452-.995-1.01-.995h-1.98a.997.997 0 0 0-1.01.995V21z"/></g></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><g fill="#8F8F8F" fill-rule="evenodd"><path d="M29.513 10.134A15.922 15.922 0 0 0 23 7.28V6h2.993C26.55 6 27 5.552 27 5V2a1 1 0 0 0-1.007-1H14.007C13.45 1 13 1.448 13 2v3a1 1 0 0 0 1.007 1H17v1.28C9.597 8.686 4 15.19 4 23c0 8.837 7.163 16 16 16s16-7.163 16-16c0-3.461-1.099-6.665-2.967-9.283l1.327-1.58a2.498 2.498 0 0 0-.303-3.53 2.499 2.499 0 0 0-3.528.315l-1.016 1.212zM20 34c6.075 0 11-4.925 11-11s-4.925-11-11-11S9 16.925 9 23s4.925 11 11 11z"/><path d="M19 21h-4.002c-.552 0-.998.452-.998 1.01v1.98c0 .567.447 1.01.998 1.01h7.004c.274 0 .521-.111.701-.291a.979.979 0 0 0 .297-.704v-8.01c0-.54-.452-.995-1.01-.995h-1.98a.997.997 0 0 0-1.01.995V21z"/></g></svg> diff --git a/app/views/shared/icons/_illustration_no_commits.svg b/app/views/shared/icons/_illustration_no_commits.svg index 4f9d9add60d..34f177d7efa 100644 --- a/app/views/shared/icons/_illustration_no_commits.svg +++ b/app/views/shared/icons/_illustration_no_commits.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg>
\ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg> diff --git a/app/views/shared/icons/_members.svg b/app/views/shared/icons/_members.svg deleted file mode 100644 index f8043b31fe8..00000000000 --- a/app/views/shared/icons/_members.svg +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="22px" height="16px" viewBox="0 0 22 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> - <title>Group</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="Group" fill="#7E7C7C"> - <path d="M6.4357,11.8588 C7.1487,11.2798 7.8797,10.7808 8.5357,10.3708 C8.5837,10.3008 8.6187,10.2338 8.6187,10.1768 L8.6187,8.8088 C8.9197,8.5218 9.0927,8.1248 9.0927,7.7028 L9.0927,5.3748 C9.0927,3.9478 7.9187,2.7858 6.4757,2.7858 L5.9687,2.7858 C4.5247,2.7858 3.3507,3.9478 3.3507,5.3748 L3.3507,7.7028 C3.3507,8.1248 3.5247,8.5218 3.8247,8.8088 L3.8247,10.5838 C3.2537,10.8738 1.8797,11.6198 0.5967,12.6618 C0.2177,12.9698 -0.0003,13.4258 -0.0003,13.9138 L-0.0003,15.5088 C-0.0003,15.5438 0.0857,15.7668 0.3467,15.7778 C1.3257,15.8198 3.8417,15.8328 5.9617,15.9038 C5.8337,15.8148 5.7447,15.6748 5.7447,15.5088 L5.7447,13.5498 C5.7447,12.9848 5.9967,12.2158 6.4357,11.8588" id="Fill-1"></path> - <path d="M21.3092,12.1 C19.6932,10.787 17.9592,9.86 17.3042,9.53 L17.3042,7.235 C17.6722,6.9 17.8862,6.428 17.8862,5.925 L17.8862,3.066 C17.8862,1.376 16.4952,0 14.7852,0 L14.1632,0 C12.4532,0 11.0622,1.376 11.0622,3.066 L11.0622,5.925 C11.0622,6.428 11.2752,6.9 11.6442,7.235 L11.6442,9.53 C10.9892,9.86 9.2542,10.787 7.6392,12.1 C7.2002,12.457 6.9482,12.985 6.9482,13.55 L6.9482,15.509 C6.9482,15.78 7.1702,16 7.4442,16 L14.1172,16 L14.1172,11.704 C12.6812,11.595 11.5652,10.853 11.5652,9.945 C11.5652,9.804 11.5982,9.669 11.6482,9.538 C11.9502,10.326 13.0982,10.913 14.4762,10.913 C15.8532,10.913 17.0012,10.326 17.3032,9.538 C17.3532,9.669 17.3862,9.804 17.3862,9.945 C17.3862,10.793 16.4152,11.5 15.1172,11.679 L15.1172,16 L21.5032,16 C21.7772,16 22.0002,15.78 22.0002,15.509 L22.0002,13.55 C22.0002,12.985 21.7482,12.457 21.3092,12.1" id="Fill-4"></path> - </g> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_milestones.svg b/app/views/shared/icons/_milestones.svg deleted file mode 100644 index 3d62ecc0631..00000000000 --- a/app/views/shared/icons/_milestones.svg +++ /dev/null @@ -1,15 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> - <title>Group</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="Group" fill="#7E7C7C"> - <path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path> - <polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon> - <polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon> - <path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path> - </g> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_mr.svg b/app/views/shared/icons/_mr.svg deleted file mode 100644 index dd3dbcc4473..00000000000 --- a/app/views/shared/icons/_mr.svg +++ /dev/null @@ -1,13 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch --> - <title>Group</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <g id="Group" fill="#7E7C7C"> - <path d="M15.1111,0 L0.8891,0 C0.3981,0 0.0001,0.446 0.0001,0.996 L0.0001,14.945 C0.0001,15.495 0.3981,15.941 0.8891,15.941 L15.1111,15.941 C15.6021,15.941 16.0001,15.495 16.0001,14.945 L16.0001,0.996 C16.0001,0.446 15.6021,0 15.1111,0 L15.1111,0 L15.1111,0 Z M2.0001,13.949 L14.0001,13.949 L14.0001,1.993 L2.0001,1.993 L2.0001,13.949 Z M2,5.0002 L14,5.0002 L14,3.0002 L2,3.0002 L2,5.0002 Z" id="Combined-Shape"></path> - <path d="M8.547,12.0002 L12,12.0002 L12,10.0002 L8.547,10.0002 L8.547,12.0002 Z M5.2029,12 L3.9999,10.867 L5.2029,9.501 L3.9999,8.181 L5.2029,7 L7.4529,9.499 L5.2029,12 Z" id="Combined-Shape"></path> - </g> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg new file mode 100644 index 00000000000..2daa55a8652 --- /dev/null +++ b/app/views/shared/icons/_mr_bold.svg @@ -0,0 +1 @@ +<svg width="15" height="20" viewBox="0 0 12 14" xmlns="http://www.w3.org/2000/svg"><path d="M1 4.967a2.15 2.15 0 1 1 2.3 0v5.066a2.15 2.15 0 1 1-2.3 0V4.967zm7.85 5.17V5.496c0-.745-.603-1.346-1.35-1.346V6l-3-3 3-3v1.85c2.016 0 3.65 1.63 3.65 3.646v4.45a2.15 2.15 0 1 1-2.3.191z" fill-rule="nonzero"/></svg> diff --git a/app/views/shared/icons/_pipelines.svg b/app/views/shared/icons/_pipelines.svg deleted file mode 100644 index 794e8a27025..00000000000 --- a/app/views/shared/icons/_pipelines.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> - <title>Pasted Image 246</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <path d="M12.5,14 C11.672,14 11,13.328 11,12.5 C11,11.672 11.672,11 12.5,11 C13.328,11 14,11.672 14,12.5 C14,13.328 13.328,14 12.5,14 M12.5,9 L3.5,9 C1.567,9 0,10.567 0,12.5 C0,14.433 1.567,16 3.5,16 L12.5,16 C14.433,16 16,14.433 16,12.5 C16,10.567 14.433,9 12.5,9 M3.5,2 C4.328,2 5,2.672 5,3.5 C5,4.328 4.328,5 3.5,5 C2.672,5 2,4.328 2,3.5 C2,2.672 2.672,2 3.5,2 M3.5,7 L12.5,7 C14.433,7 16,5.433 16,3.5 C16,1.567 14.433,0 12.5,0 L3.5,0 C1.567,0 0,1.567 0,3.5 C0,5.433 1.567,7 3.5,7" id="Pasted-Image-246" fill="#303030"></path> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_wiki.svg b/app/views/shared/icons/_wiki.svg deleted file mode 100644 index 182d91e23aa..00000000000 --- a/app/views/shared/icons/_wiki.svg +++ /dev/null @@ -1,10 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch --> - <title>Pasted Image 241</title> - <desc>Created with Sketch.</desc> - <defs></defs> - <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> - <path d="M2.004,12.9999459 L3.939,12.9999459 L3.939,4.99994585 L2.004,4.99994585 L2.004,12.9999459 Z M7.017,9.99994585 L13.018,9.99994585 L13.018,8.99994585 L7.017,8.99994585 L7.017,9.99994585 Z M7.017,7.99994585 L13.018,7.99994585 L13.018,6.99994585 L7.017,6.99994585 L7.017,7.99994585 Z M7.017,5.99994585 L13.018,5.99994585 L13.018,4.99994585 L7.017,4.99994585 L7.017,5.99994585 Z M14.754,-5.41499267e-05 L4.938,-5.41499267e-05 C4.386,-5.41499267e-05 3.938,0.44794585 3.938,0.99994585 L3.938,2.99994585 L1,2.99994585 C0.448,2.99994585 0,3.44794585 0,3.99994585 L0,12.9999459 C0.037,13.4999459 -0.25,16.0509459 3.938,15.9999459 L12.408,15.9999459 C12.408,15.9999459 15.754,15.9169459 15.754,13.9999459 L15.754,0.99994585 C15.754,0.44794585 15.306,-5.41499267e-05 14.754,-5.41499267e-05 L14.754,-5.41499267e-05 Z" id="Pasted-Image-241" fill="#7E7D7D"></path> - </g> -</svg>
\ No newline at end of file diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index b58640c3ef0..330fa8a5b10 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -25,7 +25,7 @@ %button.btn.btn-link = icon('search') %span - Keep typing and press Enter + Press Enter or click to search %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 25a4aec0a38..92d2d93a732 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -3,7 +3,7 @@ = page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('issuable') -%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } +%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => "102", "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } .issuable-sidebar - can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .block.issuable-sidebar-header @@ -13,15 +13,12 @@ %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } = sidebar_gutter_toggle_icon - if current_user - %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", "aria-label" => (todo.nil? ? "Add todo" : "Mark done"), data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } } - %span.js-issuable-todo-text - - if todo - Mark done - - else - Add todo - = icon('spin spinner', class: 'hidden js-issuable-todo-loading', 'aria-hidden': 'true') + = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f| + - if current_user + .block.todo.hide-expanded + = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true .block.assignee .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) } - if issuable.assignee @@ -121,7 +118,7 @@ - selected_labels = issuable.labels .block.labels .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } - = icon('tags', class: 'hidden', 'aria-hidden': 'true') + = icon('tags', 'aria-hidden': 'true') %span = selected_labels.size .title.hide-collapsed diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml new file mode 100644 index 00000000000..574e2958ae8 --- /dev/null +++ b/app/views/shared/issuable/_sidebar_todo.html.haml @@ -0,0 +1,15 @@ +- is_collapsed = local_assigns.fetch(:is_collapsed, false) +- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : 'Mark done' +- todo_content = is_collapsed ? icon('plus-square') : 'Add todo' + +%button.issuable-todo-btn.js-issuable-todo{ type: 'button', + class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'), + title: (todo.nil? ? 'Add todo' : 'Mark done'), + 'aria-label' => (todo.nil? ? 'Add todo' : 'Mark done'), + data: issuable_todo_button_data(issuable, todo, is_collapsed) } + %span.issuable-todo-inner.js-issuable-todo-inner< + - if todo + = mark_content + - else + = todo_content + = icon('spin spinner', 'aria-hidden': 'true') diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 8e721c9c8dd..a5aa768b1b2 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -31,7 +31,7 @@ Joined #{time_ago_with_tooltip(member.created_at)} - if member.expires? ยท - %span{ class: ('text-warning' if member.expires_soon?) } + %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) } Expires in #{distance_of_time_in_words_to_now(member.expires_at)} - else @@ -47,7 +47,7 @@ - current_resource = @project || @group .controls.member-controls - if show_controls && member.source == current_resource - - if user != current_user + - if user != current_user && can_admin_member = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.hidden_field :access_level .member-form-control.dropdown.append-right-5 diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml new file mode 100644 index 00000000000..2810f1377b2 --- /dev/null +++ b/app/views/shared/milestones/_sidebar.html.haml @@ -0,0 +1,131 @@ +- affix_offset = local_assigns.fetch(:affix_offset, "102") +- project = local_assigns[:project] + +%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' } + .issuable-sidebar.milestone-sidebar + .block.milestone-progress.issuable-sidebar-header + %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" } + = sidebar_gutter_toggle_icon + + .sidebar-collapsed-icon + %span== #{milestone.percent_complete(current_user)}% + = milestone_progress_bar(milestone) + .title.hide-collapsed + %strong.bold== #{milestone.percent_complete(current_user)}% + %span.hide-collapsed + complete + .value.hide-collapsed + = milestone_progress_bar(milestone) + + .block.start_date.hide-collapsed + .title + Start date + - if @project && can?(current_user, :admin_milestone, @project) + = link_to 'Edit', edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: 'edit-link pull-right' + .value + %span.value-content + - if milestone.start_date + %span.bold= milestone.start_date.to_s(:medium) + - else + %span.no-value No start date + + .block.due_date + .sidebar-collapsed-icon + = icon('calendar', 'aria-hidden': 'true') + %span.collapsed-milestone-date + - if milestone.start_date && milestone.due_date + - if milestone.start_date.year == milestone.due_date.year + .milestone-date= milestone.start_date.strftime('%b %-d') + - else + .milestone-date= milestone.start_date.strftime('%b %-d %Y') + .date-separator - + .due_date= milestone.due_date.strftime('%b %-d %Y') + - elsif milestone.start_date + From + .milestone-date= milestone.start_date.strftime('%b %-d %Y') + - elsif milestone.due_date + Until + .milestone-date= milestone.due_date.strftime('%b %-d %Y') + - else + None + .title.hide-collapsed + Due date + - if @project && can?(current_user, :admin_milestone, @project) + = link_to 'Edit', edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: 'edit-link pull-right' + .value.hide-collapsed + %span.value-content + - if milestone.due_date + %span.bold= milestone.due_date.to_s(:medium) + - else + %span.no-value No due date + - remaining_days = milestone_remaining_days(milestone) + - if remaining_days.present? + = surround '(', ')' do + %span.remaining-days= remaining_days + + - if !project || can?(current_user, :read_issue, project) + .block + .sidebar-collapsed-icon + %strong + = icon('hashtag', 'aria-hidden': 'true') + %span= milestone.issues_visible_to_user(current_user).count + .title.hide-collapsed + Issues + %span.badge= milestone.issues_visible_to_user(current_user).count + - if project && can?(current_user, :create_issue, project) + = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do + New issue + .value.hide-collapsed.bold + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :issues) do + Open: + = milestone.issues_visible_to_user(current_user).opened.count + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :issues, state: 'closed') do + Closed: + = milestone.issues_visible_to_user(current_user).closed.count + + .block + .sidebar-collapsed-icon + %strong + = icon('exclamation', 'aria-hidden': 'true') + %span= milestone.issues_visible_to_user(current_user).count + .title.hide-collapsed + Merge requests + %span.badge= milestone.merge_requests.count + .value.hide-collapsed.bold + - if !project || can?(current_user, :read_merge_request, project) + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :merge_requests) do + Open: + = milestone.merge_requests.opened.count + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'closed') do + Closed: + = milestone.merge_requests.closed.count + %span.milestone-stat + = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'merged') do + Merged: + = milestone.merge_requests.merged.count + - else + %span.milestone-stat + Open: + = milestone.merge_requests.opened.count + %span.milestone-stat + Closed: + = milestone.merge_requests.closed.count + %span.milestone-stat + Merged: + = milestone.merge_requests.merged.count + + - milestone_ref = milestone.try(:to_reference, full: true) + - if milestone_ref.present? + .block.reference + .sidebar-collapsed-icon.dont-change-state + = clipboard_button(clipboard_text: milestone_ref, title: "Copy reference to clipboard", placement: "left") + .cross-project-reference.hide-collapsed + %span + Reference: + %cite{ title: milestone_ref } + = milestone_ref + = clipboard_button(clipboard_text: milestone_ref, title: "Copy reference to clipboard", placement: "left") diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml deleted file mode 100644 index 78079f633d5..00000000000 --- a/app/views/shared/milestones/_summary.html.haml +++ /dev/null @@ -1,45 +0,0 @@ -- project = local_assigns[:project] - -.context.prepend-top-default - .milestone-summary - %h4 Progress - - .milestone-stats-and-buttons - .milestone-stats - - if !project || can?(current_user, :read_issue, project) - %span.milestone-stat.with-drilldown - %strong= milestone.issues_visible_to_user(current_user).size - issues: - %span.milestone-stat - %strong= milestone.issues_visible_to_user(current_user).opened.size - open and - %strong= milestone.issues_visible_to_user(current_user).closed.size - closed - %span.milestone-stat.with-drilldown - %strong= milestone.merge_requests.size - merge requests: - %span.milestone-stat - %strong= milestone.merge_requests.opened.size - open and - %strong= milestone.merge_requests.merged.size - merged - %span.milestone-stat - %strong== #{milestone.percent_complete(current_user)}% - complete - - remaining_days = milestone_remaining_days(milestone) - - if remaining_days.present? - %span.milestone-stat - %span.remaining-days= remaining_days - - .milestone-progress-buttons - %span.tab-issues-buttons - - if project - - if can?(current_user, :create_issue, project) - = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do - New Issue - - if can?(current_user, :read_issue, project) - = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn" - %span.tab-merge-requests-buttons.hidden - = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn" - - = milestone_progress_bar(milestone) diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index a0e9ec46220..9a4502873ef 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -1,26 +1,29 @@ -%ul.nav-links.no-top.no-bottom - - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) - %li.active - = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do - Issues - %span.badge= milestone.issues_visible_to_user(current_user).size +.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.nav-links.scrolling-tabs + - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) + %li.active + = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do + Issues + %span.badge= milestone.issues_visible_to_user(current_user).size + %li + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + Merge Requests + %span.badge= milestone.merge_requests.size + - else + %li.active + = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do + Merge Requests + %span.badge= milestone.merge_requests.size %li - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do - Merge Requests - %span.badge= milestone.merge_requests.size - - else - %li.active - = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do - Merge Requests - %span.badge= milestone.merge_requests.size - %li - = link_to '#tab-participants', 'data-toggle' => 'tab' do - Participants - %span.badge= milestone.participants.count - %li - = link_to '#tab-labels', 'data-toggle' => 'tab' do - Labels - %span.badge= milestone.labels.count + = link_to '#tab-participants', 'data-toggle' => 'tab' do + Participants + %span.badge= milestone.participants.count + %li + = link_to '#tab-labels', 'data-toggle' => 'tab' do + Labels + %span.badge= milestone.labels.count - show_project_name = local_assigns.fetch(:show_project_name, false) - show_full_project_name = local_assigns.fetch(:show_full_project_name, false) diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index 497446c1ef3..2562f085338 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -3,6 +3,9 @@ - group = local_assigns[:group] .detail-page-header + %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" } + = icon('angle-double-left') + .status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" } - if milestone.closed? Closed diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml index a736bfd91e2..708adbc38f1 100644 --- a/app/views/shared/notifications/_custom_notifications.html.haml +++ b/app/views/shared/notifications/_custom_notifications.html.haml @@ -25,7 +25,7 @@ .form-group .checkbox{ class: ("prepend-top-0" if index == 0) } %label{ for: field_id } - = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event]) + = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.public_send(event)) %strong = notification_event_name(event) = icon("spinner spin", class: "custom-notification-event-loading") diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index c57282c5742..c0699b13719 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -10,7 +10,7 @@ .js-projects-list-holder - if projects.any? - %ul.projects-list.content-list + %ul.projects-list - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) ? 'hide' : nil = render "shared/projects/project", project: project, skip_namespace: skip_namespace, diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index df21857e1ad..761f0b606b5 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -10,44 +10,44 @@ %li.project-row{ class: css_class } = cache(cache_key) do + - if avatar + .avatar-container.s40 + - if use_creator_avatar + = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' + - else + = project_icon(project, alt: '', class: 'avatar project-avatar s40') + .project-details + %h3.prepend-top-0.append-bottom-0 + = link_to project_path(project), class: dom_class(project) do + %span.project-full-name + %span.namespace-name + - if project.namespace && !skip_namespace + = project.namespace.human_name + \/ + %span.project-name + = project.name + + - if show_last_commit_as_description + .description.prepend-top-5 + = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit), + class: "commit-row-message" + - elsif project.description.present? + .description.prepend-top-5 + = markdown_field(project, :description) + .controls - if project.archived - %span.label.label-warning archived + %span.prepend-left-10.label.label-warning archived - if project.pipeline_status.has_status? - %span + %span.prepend-left-10 = render_project_pipeline_status(project.pipeline_status) - if forks - %span + %span.prepend-left-10 = icon('code-fork') = number_with_delimiter(project.forks_count) - if stars - %span + %span.prepend-left-10 = icon('star') = number_with_delimiter(project.star_count) - %span.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } + %span.prepend-left-10.visibility-icon.has-tooltip{ data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project) } = visibility_level_icon(project.visibility_level, fw: true) - - .title - = link_to project_path(project), class: dom_class(project) do - - if avatar - .dash-project-avatar - .avatar-container.s40 - - if use_creator_avatar - = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:'' - - else - = project_icon(project, alt: '', class: 'avatar project-avatar s40') - %span.project-full-name - %span.namespace-name - - if project.namespace && !skip_namespace - = project.namespace.human_name - \/ - %span.project-name.filter-title - = project.name - - - if show_last_commit_as_description - .description - = link_to_gfm project.commit.title, namespace_project_commit_path(project.namespace, project, project.commit), - class: "commit-row-message" - - elsif project.description.present? - .description - = markdown_field(project, :description) diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 4afd31f788b..d1e88274878 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -18,9 +18,9 @@ = event_action_name(event) %strong - if event.note? - = link_to event.note_target.to_reference, event_note_target_path(event) + = link_to event.note_target.to_reference, event_note_target_path(event), class: 'has-tooltip', title: event.target_title - elsif event.target - = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target] + = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title at %strong diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index dc9a3b0d0df..969ea7ab9e6 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -13,7 +13,7 @@ .cover-block.user-cover-block .cover-controls - if @user == current_user - = link_to profile_path, class: 'btn btn-gray' do + = link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do = icon('pencil') - elsif current_user - if @user.abuse_report @@ -24,7 +24,7 @@ = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn btn-gray', title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('exclamation-circle') - = link_to user_path(@user, rss_url_options), class: 'btn btn-gray' do + = link_to user_path(@user, rss_url_options), class: 'btn btn-gray has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do = icon('rss') - if current_user && current_user.admin? = link_to [:admin, @user], class: 'btn btn-gray', title: 'View user in admin area', @@ -44,7 +44,7 @@ %span.middle-dot-divider @#{@user.username} %span.middle-dot-divider - Member since #{@user.created_at.to_s(:medium)} + Member since #{@user.created_at.to_date.to_s(:long)} .cover-desc - unless @user.public_email.blank? @@ -79,25 +79,29 @@ %p.profile-user-bio = @user.bio - %ul.nav-links.center.user-profile-nav - %li.js-activity-tab - = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do - Activity - %li.js-groups-tab - = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do - Groups - %li.js-contributed-tab - = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do - Contributed projects - %li.js-projects-tab - = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do - Personal projects - %li.js-snippets-tab - = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do - Snippets + .scrolling-tabs-container + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.nav-links.center.user-profile-nav.scrolling-tabs + %li.js-activity-tab + = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do + Activity + %li.js-groups-tab + = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do + Groups + %li.js-contributed-tab + = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do + Contributed projects + %li.js-projects-tab + = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do + Personal projects + %li.js-snippets-tab + = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do + Snippets %div{ class: container_class } - .user-callout{ 'callout-svg' => custom_icon('icon_customization') } + - if @user == current_user && !show_user_callout? + = render 'shared/user_callout' .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 2cd87895c55..015a41b6e82 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -3,20 +3,16 @@ class PostReceive include DedicatedSidekiqQueue def perform(repo_path, identifier, changes) - if repository_storage = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1]['path'].to_s) } - repo_path.gsub!(repository_storage[1]['path'].to_s, "") - else - log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"") - end + repo_relative_path = Gitlab::RepoPath.strip_storage_path(repo_path) changes = Base64.decode64(changes) unless changes.include?(' ') # Use Sidekiq.logger so arguments can be correlated with execution # time and thread ID's. Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS'] - post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes) + post_received = Gitlab::GitPostReceive.new(repo_relative_path, identifier, changes) if post_received.project.nil? - log("Triggered hook for non-existing project with full path \"#{repo_path}\"") + log("Triggered hook for non-existing project with full path \"#{repo_relative_path}\"") return false end @@ -25,7 +21,7 @@ class PostReceive elsif post_received.regular_project? process_project_changes(post_received) else - log("Triggered hook for unidentifiable repository type with full path \"#{repo_path}\"") + log("Triggered hook for unidentifiable repository type with full path \"#{repo_relative_path}\"") false end end diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index e9a5bd7f24e..2f7967cf531 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -53,6 +53,8 @@ class ProcessCommitWorker def update_issue_metrics(commit, author) mentioned_issues = commit.all_references(author).issues + return if mentioned_issues.empty? + Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil). update_all(first_mentioned_in_commit_at: commit.committed_date) end diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index c8a77e21c12..68a6fd76e70 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -1,6 +1,5 @@ class RepositoryImportWorker include Sidekiq::Worker - include Gitlab::ShellAdapter include DedicatedSidekiqQueue attr_accessor :project, :current_user |