diff options
author | Stan Hu <stanhu@gmail.com> | 2017-03-24 15:02:23 -0700 |
---|---|---|
committer | Stan Hu <stanhu@gmail.com> | 2017-03-24 15:02:23 -0700 |
commit | e1bb8c99cf7840c8b5d981111857b7d16b9cc1bd (patch) | |
tree | 9737570f75dbb84a325537cd1863301e42234fee /app | |
parent | f6ededfa07a2fe9f3dd1c4d7e247b0f784f8181a (diff) | |
parent | f1e1e51311b6f50c98b3e1476942107269c28a97 (diff) | |
download | gitlab-ce-e1bb8c99cf7840c8b5d981111857b7d16b9cc1bd.tar.gz |
Merge branch 'master' into sh-bring-back-option-to-be-notified-of-own-activity
Diffstat (limited to 'app')
251 files changed, 1868 insertions, 1219 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..c743dd551d7 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'; diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 92f3bb3ff52..86927314dd4 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -24,7 +24,7 @@ $('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/template_selector.js b/app/assets/javascripts/blob/template_selector.js deleted file mode 100644 index 7e03ec3b391..00000000000 --- a/app/assets/javascripts/blob/template_selector.js +++ /dev/null @@ -1,101 +0,0 @@ -/* 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); - } - - 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; - } - }); - } - - bindEvents() { - return this.$input.on('keyup blur', (e) => this.onFilenameUpdate()); - } - - toggleLabel(item) { - return 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'); - } - - onClick(item, el, e) { - e.preventDefault(); - return this.requestFile(item); - } - - requestFile(item) { - // This `requestFile` method is an abstract method that should - // be added by all subclasses. - } - - // To be implemented on the extending class - // e.g. - // Api.gitignoreText item.name, @requestFileSuccess.bind(@) - requestFileSuccess(file, { skipFocus } = {}) { - if (!file) return; - - const oldValue = this.editor.getValue(); - const newValue = file.content; - - this.editor.setValue(newValue, 1); - if (!skipFocus) this.editor.focus(); - - if (this.editor instanceof jQuery) { - this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); - } - } - - startLoadingSpinner() { - this.dropdownIcon - .addClass('fa-spinner fa-spin') - .removeClass('fa-chevron-down'); - } - - stopLoadingSpinner() { - this.dropdownIcon - .addClass('fa-chevron-down') - .removeClass('fa-spinner fa-spin'); - } - } - - global.TemplateSelector = TemplateSelector; -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js new file mode 100644 index 00000000000..5a5954e7751 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js @@ -0,0 +1,9 @@ +/* global Api */ + +import TemplateSelector from './template_selector'; + +export default class BlobCiYamlSelector extends TemplateSelector { + requestFile(query) { + return Api.gitlabCiYml(query.name, (file, config) => this.setEditorContent(file, config)); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js new file mode 100644 index 00000000000..7a4d6a42a03 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js @@ -0,0 +1,23 @@ +/* global Api */ + +import BlobCiYamlSelector from './blob_ci_yaml_selector'; + +export default class BlobCiYamlSelectors { + constructor({ editor, $dropdowns }) { + this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector'); + this.initSelectors(editor); + } + + initSelectors(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, + }); + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js new file mode 100644 index 00000000000..19f8820a0cb --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js @@ -0,0 +1,9 @@ +/* global Api */ + +import TemplateSelector from './template_selector'; + +export default class BlobDockerfileSelector extends TemplateSelector { + requestFile(query) { + return Api.dockerfileYml(query.name, (file, config) => this.setEditorContent(file, config)); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js new file mode 100644 index 00000000000..da067035b43 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js @@ -0,0 +1,23 @@ +import BlobDockerfileSelector from './blob_dockerfile_selector'; + +export default 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 BlobDockerfileSelector({ + editor, + pattern: /(Dockerfile)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'), + dropdown: $dropdown, + }); + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js new file mode 100644 index 00000000000..0b6b02fc2b3 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js @@ -0,0 +1,9 @@ +/* global Api */ + +import TemplateSelector from './template_selector'; + +export default class BlobGitignoreSelector extends TemplateSelector { + requestFile(query) { + return Api.gitignoreText(query.name, (file, config) => this.setEditorContent(file, config)); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js new file mode 100644 index 00000000000..dc485d97677 --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js @@ -0,0 +1,23 @@ +import BlobGitignoreSelector from './blob_gitignore_selector'; + +export default class BlobGitignoreSelectors { + constructor({ editor, $dropdowns }) { + this.$dropdowns = $dropdowns || $('.js-gitignore-selector'); + this.editor = editor; + this.initSelectors(); + } + + initSelectors() { + this.$dropdowns.each((i, dropdown) => { + const $dropdown = $(dropdown); + + return new BlobGitignoreSelector({ + pattern: /(.gitignore)/, + data: $dropdown.data('data'), + wrapper: $dropdown.closest('.js-gitignore-selector-wrap'), + dropdown: $dropdown, + editor: this.editor, + }); + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selector.js b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js new file mode 100644 index 00000000000..e9cb31cc2dc --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js @@ -0,0 +1,13 @@ +/* global Api */ + +import TemplateSelector from './template_selector'; + +export default class BlobLicenseSelector extends TemplateSelector { + requestFile(query) { + const data = { + project: this.dropdown.data('project'), + fullname: this.dropdown.data('fullname'), + }; + return Api.licenseText(query.id, data, (file, config) => this.setEditorContent(file, config)); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js new file mode 100644 index 00000000000..a44f4f78b2d --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js @@ -0,0 +1,24 @@ +/* eslint-disable no-unused-vars, no-param-reassign */ + +import BlobLicenseSelector from './blob_license_selector'; + +export default class BlobLicenseSelectors { + constructor({ $dropdowns, editor }) { + this.$dropdowns = $dropdowns || $('.js-license-selector'); + this.initSelectors(editor); + } + + initSelectors(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, + }); + }); + } +} diff --git a/app/assets/javascripts/blob/template_selectors/template_selector.js b/app/assets/javascripts/blob/template_selectors/template_selector.js new file mode 100644 index 00000000000..d7c1c32efbd --- /dev/null +++ b/app/assets/javascripts/blob/template_selectors/template_selector.js @@ -0,0 +1,92 @@ +/* eslint-disable class-methods-use-this, no-unused-vars */ + +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); + + this.initDropdown(dropdown, data); + this.listenForFilenameInput(); + this.renderMatchedDropdown(); + this.initAutosizeUpdateEvent(); + } + + 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, + }); + } + + initAutosizeUpdateEvent() { + this.autosizeUpdateEvent = document.createEvent('Event'); + this.autosizeUpdateEvent.initEvent('autosize:update', true, false); + } + + listenForFilenameInput() { + return this.$filenameInput.on('keyup blur', e => this.renderMatchedDropdown(e)); + } + + renderMatchedDropdown() { + if (!this.$filenameInput.length) { + return null; + } + + const filenameMatches = this.pattern.test(this.$filenameInput.val().trim()); + + if (!filenameMatches) { + return this.$dropdownContainer.addClass('hidden'); + } + return this.$dropdownContainer.removeClass('hidden'); + } + + fetchFileTemplate(item, el, e) { + e.preventDefault(); + return this.requestFile(item); + } + + requestFile(item) { + // This `requestFile` method is an abstract method that should + // be added by all subclasses. + } + + // To be implemented on the extending class + // e.g. Api.gitlabCiYml(query.name, file => this.setEditorContent(file)); + + setEditorContent(file, { skipFocus } = {}) { + if (!file) return; + + const newValue = file.content; + + this.editor.setValue(newValue, 1); + + if (!skipFocus) this.editor.focus(); + + if (this.editor instanceof jQuery) { + this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); + } + } + + 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_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js new file mode 100644 index 00000000000..c5deccf631e --- /dev/null +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -0,0 +1,32 @@ +/* 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'); + + new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage); + 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..d3560d5df3b 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,88 +1,99 @@ -/* 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 - }); + +import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors'; +import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors'; +import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors'; +import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors'; + +export default class EditBlob { + constructor(assetsPath, aceMode) { + this.configureAceEditor(aceMode, assetsPath); + this.prepFileContentForSubmit(); + this.initModePanesAndLinks(); + this.initSoftWrap(); + this.initFileSelectors(); + } + + configureAceEditor(aceMode, assetsPath) { + ace.config.set('modePath', `${assetsPath}/ace`); + ace.config.loadModule('ace/ext/searchbox'); + + this.editor = ace.edit('editor'); + this.editor.focus(); + + if (aceMode) { + this.editor.getSession().setMode(`ace/mode/${aceMode}`); + } + } + + prepFileContentForSubmit() { + $('form').submit(() => { + $('#file-content').val(this.editor.getValue()); + }); + } + + initFileSelectors() { + this.blobTemplateSelectors = [ + new BlobLicenseSelectors({ + editor: this.editor, + }), new BlobGitignoreSelectors({ - editor: this.editor - }); - new gl.BlobCiYamlSelectors({ - editor: this.editor - }); - new gl.BlobDockerfileSelectors({ - editor: this.editor + editor: this.editor, + }), + new BlobCiYamlSelectors({ + editor: this.editor, + }), + new BlobDockerfileSelectors({ + 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..149bfbc8e8b 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; diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 67c0c419713..35b3205cca7 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 boardBlankState from './board_blank_state'; require('./board_delete'); 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..9320848bcca 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; 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..86e6c26e570 100644 --- a/app/assets/javascripts/boards/components/board_list.js +++ b/app/assets/javascripts/boards/components/board_list.js @@ -1,7 +1,7 @@ /* eslint-disable comma-dangle, space-before-function-paren, max-len */ -/* global Vue */ /* global Sortable */ +import Vue from 'vue'; import boardNewIssue from './board_new_issue'; import boardCard from './board_card'; @@ -48,7 +48,7 @@ import boardCard from './board_card'; this.list.getIssues(false); } - if (this.scrollHeight() > this.listHeight()) { + if (this.scrollHeight() > Math.ceil(this.listHeight())) { this.showCount = true; } else { this.showCount = false; 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..ba44dc5ed94 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'; (() => { 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 1b66c8b922d..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,7 +65,15 @@ require('./empty_state'); }, filter: { handler() { - this.loadIssues(true); + if (this.$el.tagName) { + this.page = 1; + this.filterLoading = true; + + this.loadIssues(true) + .then(() => { + this.filterLoading = false; + }); + } }, deep: true, }, @@ -115,6 +124,9 @@ require('./empty_state'); return this.activeTab === 'selected' && this.selectedIssues.length === 0; }, }, + created() { + this.page = 1; + }, components: { 'modal-header': gl.issueBoards.ModalHeader, 'modal-list': gl.issueBoards.ModalList, @@ -135,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..d8322b34d44 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; 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/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..8912f234aa6 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 || {}; 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..a20e5bc3b1b 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, }, /** @@ -36,12 +38,24 @@ export default Vue.component('pipelines-table', { return { endpoint: pipelinesTableData.endpoint, + helpPagePath: pipelinesTableData.helpPagePath, 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 * @@ -80,26 +94,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/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js index b83a4c63fad..9947f355aca 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 || {}; 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..6ad4805e8c5 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 || {}; 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..da80450a32c 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 || {}; 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..2200f43914f 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 || {}; 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..ae17d05e679 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -1,9 +1,8 @@ -/* global Vue */ -/* global Cookies */ /* global Flash */ -window.Vue = require('vue'); -window.Cookies = require('js-cookie'); +import Vue from 'vue'; +import Cookies from 'js-cookie'; + require('./components/stage_code_component'); require('./components/stage_issue_component'); require('./components/stage_plan_component'); 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 3557f6f617e..d1a662459e1 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -41,9 +41,9 @@ 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'; const ShortcutsBlob = require('./shortcuts_blob'); -const UserCallout = require('./user_callout'); (function() { var Dispatcher; 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_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js index a554998f52c..b4f9eb357fd 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js +++ b/app/assets/javascripts/environments/components/environment_external_url.js @@ -14,6 +14,7 @@ export default { class="btn external_url" :href="externalUrl" target="_blank" + rel="noopener noreferrer" title="Environment external URL"> <i class="fa fa-external-link" aria-hidden="true"></i> </a> diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 7ace51748aa..22352950452 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -40,6 +40,8 @@ import FilteredSearchContainer from './container'; this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); this.editTokenWrapper = this.editToken.bind(this); this.tokenChange = this.tokenChange.bind(this); + this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this); + this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this); this.filteredSearchInputForm = this.filteredSearchInput.form; this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); @@ -51,11 +53,13 @@ import FilteredSearchContainer from './container'; this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); this.filteredSearchInput.addEventListener('click', this.tokenChange); this.filteredSearchInput.addEventListener('keyup', this.tokenChange); + this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper); this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.addEventListener('click', this.unselectEditTokensWrapper); + document.addEventListener('click', this.removeInputContainerFocusWrapper); document.addEventListener('keydown', this.removeSelectedTokenWrapper); } @@ -69,11 +73,13 @@ import FilteredSearchContainer from './container'; this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); this.filteredSearchInput.removeEventListener('click', this.tokenChange); this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); + this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper); this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.removeEventListener('click', this.unselectEditTokensWrapper); + document.removeEventListener('click', this.removeInputContainerFocusWrapper); document.removeEventListener('keydown', this.removeSelectedTokenWrapper); } @@ -124,6 +130,26 @@ import FilteredSearchContainer from './container'; } } + addInputContainerFocus() { + const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container'); + + if (inputContainer) { + inputContainer.classList.add('focus'); + } + } + + removeInputContainerFocus(e) { + const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container'); + const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target); + const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null; + const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null; + + if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown && + !isElementInStaticFilterDropdown && inputContainer) { + inputContainer.classList.remove('focus'); + } + } + static selectToken(e) { const button = e.target.closest('.selectable'); @@ -358,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/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/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js new file mode 100644 index 00000000000..c30a1fcb5da --- /dev/null +++ b/app/assets/javascripts/lib/utils/poll.js @@ -0,0 +1,73 @@ +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'}, + * successCallback: () => {}, + * errorCallback: () => {}, + * }).makeRequest(); + * + * this.service = new BoardsService(endpoint); + * new poll({ + * resource: this.service, + * method: 'get', + * data: {page: 1, scope: 'all'}, + * successCallback: () => {}, + * errorCallback: () => {}, + * }).makeRequest(); + * + * + * 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.intervalHeader = 'POLL-INTERVAL'; + this.timeoutID = null; + this.canPoll = true; + } + + checkConditions(response) { + const headers = gl.utils.normalizeHeaders(response.headers); + const pollInterval = headers[this.intervalHeader]; + + 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 } = this.options; + + 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); + } +} diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 81d5748191d..9007d661d01 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'; 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..811f90c5a87 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,10 +1,10 @@ /* eslint-disable no-new, class-methods-use-this */ /* global Breakpoints */ -/* global Cookies */ /* global Flash */ +import Cookies from 'js-cookie'; + require('./breakpoints'); -window.Cookies = require('js-cookie'); require('./flash'); /* eslint-disable max-len */ @@ -127,9 +127,6 @@ 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; diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index 94a4f24f1d7..0e2af3df071 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -14,13 +14,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; <%= ci_success_icon %> <span> Deployed to - <a href="<%- url %>" target="_blank" class="environment"> + <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment"> <%- name %> </a> <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>"> <%- deployed_at %> </span> - <a class="js-environment-link" href="<%- external_url %>" target="_blank"> + <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer"> <i class="fa fa-external-link"></i> View on <%- external_url_formatted %> </a> 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..844a0785bc9 100644 --- a/app/assets/javascripts/monitoring/prometheus_graph.js +++ b/app/assets/javascripts/monitoring/prometheus_graph.js @@ -1,4 +1,4 @@ -/* eslint-disable no-new */ +/* eslint-disable no-new*/ /* global Flash */ import d3 from 'd3'; @@ -180,7 +180,7 @@ class PrometheusGraph { // Metric Usage axisLabelContainer.append('rect') .attr('x', this.originalWidth - 170) - .attr('y', (this.originalHeight / 2) - 80) + .attr('y', (this.originalHeight / 2) - 60) .style('fill', graphSpecifics.area_fill_color) .attr('width', 20) .attr('height', 35); @@ -188,13 +188,13 @@ class PrometheusGraph { 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('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('y', (this.originalHeight / 2) - 25); } handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) { @@ -263,12 +263,12 @@ class PrometheusGraph { cpu_values: { area_fill_color: '#edf3fc', line_color: '#5b99f7', - graph_legend_title: 'CPU Usage (Cores)', + graph_legend_title: 'CPU utilization (%)', }, memory_values: { area_fill_color: '#fca326', line_color: '#fc6d26', - graph_legend_title: 'Memory Usage (MB)', + graph_legend_title: 'Memory usage (MB)', }, }; 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/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/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..64a68d56962 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); }; }; @@ -199,7 +200,7 @@ Sidebar.prototype.setSidebarHeight = function() { const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').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..32067ed1fee 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_selectors/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/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..b27d252a3ef 100644 --- a/app/assets/javascripts/user_callout.js +++ b/app/assets/javascripts/user_callout.js @@ -1,4 +1,4 @@ -/* global Cookies */ +import Cookies from 'js-cookie'; const userCalloutElementName = '.user-callout'; const closeButton = '.close-user-callout'; @@ -27,7 +27,7 @@ const USER_CALLOUT_TEMPLATE = ` </div> </div>`; -class UserCallout { +export default class UserCallout { constructor() { this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE); this.userCalloutBody = $(userCalloutElementName); @@ -56,5 +56,3 @@ class UserCallout { } } } - -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/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..b4480bd98c7 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js @@ -0,0 +1,68 @@ +export default { + props: { + scope: { + type: String, + required: true, + }, + + count: { + type: Object, + required: true, + }, + + paths: { + type: Object, + required: true, + }, + }, + + template: ` + <ul class="nav-links"> + <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/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..48f0e9036e8 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,72 @@ 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" + v-if="!isLoading && !shouldRenderEmptyState"> + <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_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/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/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index cda46223492..50849e95541 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); } } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 186bb9ac616..da5b754aec7 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -195,7 +195,7 @@ text-decoration: none; .badge { - background-color: darken($row-hover, 5%); + background-color: darken($dropdown-link-hover-bg, 5%); } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 6bdaa7cdd6f..51805c5d734 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -173,17 +173,26 @@ } } + &:hover { + @extend .form-control:hover; + } + + &.focus, + &.focus:hover { + border-color: $dropdown-input-focus-border; + box-shadow: 0 0 4px $search-input-focus-shadow-color; + } + + &.focus .fa-filter { + color: $common-gray-dark; + } + .form-control { position: relative; min-width: 200px; - padding-left: 0; - padding-right: 25px; + padding: 5px 25px 6px 0; border-color: transparent; - &:focus ~ .fa-filter { - color: $common-gray-dark; - } - &:focus, &:hover { outline: none; 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..fa02598760f 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -265,7 +265,7 @@ header { } .impersonation i { - color: $red-normal; + color: $red-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/nav.scss b/app/assets/stylesheets/framework/nav.scss index 205d23b1329..5ab505034b6 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -416,14 +416,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; + } } } } 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..575d32b1a23 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -487,9 +487,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/environments.scss b/app/assets/stylesheets/pages/environments.scss index 73a5da715f2..25be7f408d0 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -18,7 +18,10 @@ .environments-container { .table-holder { width: 100%; - overflow: auto; + + @media (max-width: $screen-sm-max) { + overflow: auto; + } } .table.ci-table { diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4426169ef5a..c1a9bc4be28 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -498,7 +498,7 @@ svg { width: 16px; height: 16px; - fill: $sidebar-collapsed-icon-color; + fill: $issuable-sidebar-color; } &:hover svg { @@ -520,12 +520,12 @@ &.over_estimate { .meter-fill { - background: $red-light; + background: $red-500; } .time-remaining, .compare-value.spent { - color: $red-light; + color: $red-500; } } } 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..6630904ec92 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -535,7 +535,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..efbd9365fd9 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -63,7 +63,7 @@ } .remaining-days { - color: $orange-light; + color: $orange-600; } .milestone-stats-and-buttons { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e238f0865f6..a2129722633 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -462,17 +462,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 efa47be9a73..949d52cffa2 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -582,54 +582,55 @@ 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; - - .project-full-name { - @include str-truncated; + display: flex; + align-items: center; + } - @media (max-width: $screen-xs-max) { - max-width: 50%; - } - } + h3 { + font-size: $gl-font-size; + } - .controls { - line-height: $list-text-height; + a { + color: $gl-text-color; + } - .badge { - @media (max-width: $screen-xs-max) { - display: none; - } - } + .avatar-container, + .controls { + flex: 0 0 auto; + } - a:hover { - text-decoration: none; - } + .avatar-container { + align-self: flex-start; + } - > span { - margin-left: 10px; - } + .project-details { + min-width: 0; - svg { - position: relative; - top: 2px; - } + p, + .commit-row-message { + @include str-truncated(100%); + margin-bottom: 0; } + } - .description p { - @media (max-width: $screen-xs-max) { - max-width: 50%; - } - } + .controls { + margin-left: auto; } - .bottom { - padding-top: $gl-padding; - padding-bottom: 0; + .ci-status-link { + display: inline-block; + line-height: 17px; + vertical-align: middle; + + &:hover { + text-decoration: none; + } } } 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..b071d7f18cd 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -47,6 +47,7 @@ .todo-avatar, .todo-actions { + @include transition(opacity); -webkit-flex: 0 0 auto; flex: 0 0 auto; } @@ -67,21 +68,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/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/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 096de8032ae..498690e8f11 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -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/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/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..f1e4246e7fb 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 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/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 491cd5dc351..0d6d9f492c1 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -148,7 +148,14 @@ class Projects::IssuesController < Projects::ApplicationController end format.json do - render json: @issue.to_json(include: { milestone: {}, assignee: { 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 diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 82f9b6e06db..2fadf7c8c81 100644..100755 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -308,7 +308,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end format.json do - render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) end end rescue ActiveRecord::StaleObjectError @@ -402,7 +402,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 +413,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 diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index be52b0fa7cf..5922e686cd0 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -13,11 +13,14 @@ 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 + @sort = params[:sort] || 'due_date_asc' + @milestones = @milestones.sort(@sort) + @milestones = @milestones.includes(:project) respond_to do |format| format.html do 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/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/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/blob_helper.rb b/app/helpers/blob_helper.rb index 0b0c6a07efd..8631bc54509 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -215,6 +215,6 @@ module BlobHelper end def open_raw_file_button(path) - link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', title: 'Open raw', data: { container: 'body' } + link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' } end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 8aad39e148b..cef624430da 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -211,7 +211,7 @@ module CommitsHelper external_url = environment.external_url_for(diff_new_path, commit_sha) return unless external_url - link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do + link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do icon('external-link') end end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index a0642a1894b..a57b5a8fea5 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -7,7 +7,7 @@ module ImportHelper def provider_project_link(provider, path_with_namespace) url = __send__("#{provider}_project_url", path_with_namespace) - link_to path_with_namespace, url, target: '_blank' + link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer' end private 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..991fd949b94 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -31,7 +31,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/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/models/application_setting.rb b/app/models/application_setting.rb index be632930895..671a0fe98cc 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -163,6 +163,8 @@ class ApplicationSetting < ActiveRecord::Base end def self.current + ensure_cache_setup + Rails.cache.fetch(CACHE_KEY) do ApplicationSetting.last end @@ -176,9 +178,16 @@ class ApplicationSetting < ActiveRecord::Base end def self.cached + ensure_cache_setup Rails.cache.fetch(CACHE_KEY) end + def self.ensure_cache_setup + # This is a workaround for a Rails bug that causes attribute methods not + # to be loaded when read from cache: https://github.com/rails/rails/issues/27348 + ApplicationSetting.define_attribute_methods + end + def self.defaults_ce { after_sign_up_text: nil, 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/issuable.rb b/app/models/concerns/issuable.rb index 91f4eb13ecc..4d54426b79e 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -48,11 +48,14 @@ module Issuable delegate :name, :email, + :public_email, to: :author, + allow_nil: true, prefix: true delegate :name, :email, + :public_email, to: :assignee, allow_nil: true, prefix: true 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/event.rb b/app/models/event.rb index d7ca8e3c599..5c34844b5d3 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -16,7 +16,7 @@ class Event < ActiveRecord::Base RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour - delegate :name, :email, to: :author, prefix: true, allow_nil: true + delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :note, prefix: true, allow_nil: true 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..10a5d9d2a24 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -211,9 +211,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 = {}) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index cef8ad76b07..5ff83944d8c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -7,7 +7,6 @@ class MergeRequest < ActiveRecord::Base 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 +154,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 +542,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/milestone.rb b/app/models/milestone.rb index c0deb59ec4c..e85d5709624 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -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..826ded22ae5 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -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 diff --git a/app/models/project.rb b/app/models/project.rb index 17cf8226bcc..f1bba56d32c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -196,6 +196,7 @@ class Project < ActiveRecord::Base validates :name, uniqueness: { scope: :namespace_id } validates :path, uniqueness: { scope: :namespace_id } validates :import_url, addressable_url: true, if: :external_import? + validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create validate :avatar_type, @@ -237,7 +238,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! @@ -313,20 +314,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)) @@ -880,13 +876,9 @@ class Project < ActiveRecord::Base end def http_url_to_repo(user = nil) - url = web_url + credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user) - if user - url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" } - end - - "#{url}.git" + Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url end # Check if current branch name is marked as protected in the system 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/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/prometheus_service.rb b/app/models/project_services/prometheus_service.rb index 375966b9efc..5cff9a42484 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="app",environment="#{environment_slug}"}) / count(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])) / count(container_cpu_usage_seconds_total{container_name="app",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_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/route.rb b/app/models/route.rb index 73574a6206b..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 = {} @@ -21,7 +23,7 @@ class Route < ActiveRecord::Base attributes[:path] = route.path.sub(path_was, path) end - if name_changed? && route.name.present? + if name_changed? && name_was.present? && route.name.present? attributes[:name] = route.name.sub(name_was, name) 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/user.rb b/app/models/user.rb index 8c7ad5d5174..5d19d873f43 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 83f51947bd4..cb6d30396ec 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 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/projects/import_service.rb b/app/services/projects/import_service.rb index 1c5a549feb9..d484a96f785 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -33,6 +33,7 @@ module Projects def import_repository 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 # Expire cache to prevent scenarios such as: @@ -40,7 +41,7 @@ module Projects # 2. Retried import, repo is broken or not imported but +exists?+ still returns true project.repository.before_import if project.repository_exists? - raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" + raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" end 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_hooks_service.rb b/app/services/system_hooks_service.rb index 868fa7b3f21..af0ddbe5934 100644 --- a/app/services/system_hooks_service.rb +++ b/app/services/system_hooks_service.rb @@ -24,10 +24,9 @@ class SystemHooksService key: model.key, id: model.id ) + if model.user - data.merge!( - username: model.user.username - ) + data[:username] = model.user.username end when Project data.merge!(project_data(model)) @@ -35,8 +34,6 @@ class SystemHooksService if event == :rename || event == :transfer data[:old_path_with_namespace] = model.old_path_with_namespace end - - data when User data.merge!({ name: model.name, @@ -59,6 +56,8 @@ class SystemHooksService when GroupMember data.merge!(group_member_data(model)) end + + data end def build_event_name(model, event) diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb new file mode 100644 index 00000000000..37a314adee6 --- /dev/null +++ b/app/validators/importable_url_validator.rb @@ -0,0 +1,11 @@ +# ImportableUrlValidator +# +# This validator blocks projects from using dangerous import_urls to help +# protect against Server-side Request Forgery (SSRF). +class ImportableUrlValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + if Gitlab::UrlBlocker.blocked_url?(value) + record.errors.add(attribute, "imports are not allowed from that URL") + end + end +end diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 9175b3d3f96..e403a9da616 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -48,7 +48,7 @@ .form-actions = f.submit 'Save', class: 'btn btn-save append-right-10' - if @appearance.persisted? - = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank' + = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer' - if @appearance.updated_at %span.pull-right diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 00366b0a8c9..3eab065bb9f 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -404,7 +404,7 @@ Enable Sentry .help-block Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: - %a{ href: 'https://getsentry.com', target: '_blank' } https://getsentry.com + %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com .form-group = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2' 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/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/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index d31ced004a0..e31fa5fbe95 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 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/events/_event.atom.builder b/app/views/events/_event.atom.builder index 43a52cf3002..158061579f6 100644 --- a/app/views/events/_event.atom.builder +++ b/app/views/events/_event.atom.builder @@ -9,7 +9,7 @@ xml.entry do xml.author do xml.name event.author_name - xml.email event.author_email + xml.email event.author_public_email end xml.summary(type: "xhtml") do |summary| diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index f08c96df309..64b5a733b77 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -15,6 +15,6 @@ = link_to note.attachment.url, target: '_blank' do = image_tag note.attachment.url, class: 'note-image-attach' - else - = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do + = link_to note.attachment.url, target: '_blank', class: 'note-file-attach' do %i.fa.fa-paperclip = note.attachment_identifier 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/help/index.html.haml b/app/views/help/index.html.haml index 31631887317..f93b6b63426 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -17,7 +17,7 @@ %br Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. %br - Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}. + Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. - if current_application_settings.help_page_text.present? %hr = markdown_field(current_application_settings, :help_page_text) diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index e18bd47798b..e6058617ac9 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -33,7 +33,7 @@ - @already_added_projects.each do |project| %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td - = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank' + = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank', rel: 'noopener noreferrer' %td = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status @@ -50,7 +50,7 @@ - @repos.each do |repo| %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } %td - = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank" + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' %td.import-target %fieldset.row .input-group @@ -70,7 +70,7 @@ - @incompatible_repos.each do |repo| %tr{ id: "repo_#{repo.owner}___#{repo.slug}" } %td - = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank' + = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer' %td.import-target %td.import-actions-job-status = label_tag 'Incompatible Project', nil, class: 'label label-danger' diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml index d5b88709a34..7456799ca0e 100644 --- a/app/views/import/gitlab/status.html.haml +++ b/app/views/import/gitlab/status.html.haml @@ -43,7 +43,7 @@ - @repos.each do |repo| %tr{ id: "repo_#{repo["id"]}" } %td - = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank" + = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank", rel: 'noopener noreferrer' %td.import-target = import_project_target(repo['namespace']['path'], repo['name']) %td.import-actions.job-status diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml index 336becd229e..c5800a1cca0 100644 --- a/app/views/import/google_code/new.html.haml +++ b/app/views/import/google_code/new.html.haml @@ -13,7 +13,7 @@ %li %p Go to - #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: "_blank"}. + #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: '_blank', rel: 'noopener noreferrer'}. %li %p Make sure you're logged into the account that owns the projects you'd like to import. diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 5e01af008be..60de6bfe816 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -36,7 +36,7 @@ - @already_added_projects.each do |project| %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" } %td - = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank" + = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank", rel: 'noopener noreferrer' %td = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project] %td.job-status @@ -53,7 +53,7 @@ - @repos.each do |repo| %tr{ id: "repo_#{repo.id}" } %td - = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer' %td.import-target #{current_user.username}/#{repo.name} %td.import-actions.job-status @@ -63,7 +63,7 @@ - @incompatible_repos.each do |repo| %tr{ id: "repo_#{repo.id}" } %td - = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" + = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer' %td.import-target %td.import-actions-job-status = label_tag "Incompatible Project", nil, class: "label label-danger" diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder index fcd30c8c765..23a88448055 100644 --- a/app/views/issues/_issue.atom.builder +++ b/app/views/issues/_issue.atom.builder @@ -7,7 +7,7 @@ xml.entry do xml.author do xml.name issue.author_name - xml.email issue.author_email + xml.email issue.author_public_email end xml.summary issue.title @@ -26,7 +26,7 @@ xml.entry do if issue.assignee xml.assignee do xml.name issue.assignee.name - xml.email issue.assignee.email + xml.email issue.assignee_public_email end end end diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml index 65887aacbaf..04e2d4b63e6 100644 --- a/app/views/koding/index.html.haml +++ b/app/views/koding/index.html.haml @@ -2,5 +2,5 @@ %p = icon('circle', class: 'cgreen') Integration is active for - = link_to koding_project_url, target: '_blank' do + = link_to koding_project_url, target: '_blank', rel: 'noopener noreferrer' do #{current_application_settings.koding_url} 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..7ddee0e5244 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -32,7 +32,7 @@ = 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 + = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-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) 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/profiles/show.html.haml b/app/views/profiles/show.html.haml index d551754a2e5..c74b3249a13 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -18,7 +18,7 @@ or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} .col-lg-9 .clearfix.avatar-image.append-bottom-default - = link_to avatar_icon(@user, 400), target: '_blank' do + = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' %h5.prepend-top-0 Upload new avatar diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index f864702d862..ea3cecb86a9 100644 --- a/app/views/projects/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -9,7 +9,7 @@ - else .nothing-here-block The SVG could not be displayed as it is too large, you can - #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')} + #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')} instead. - else %img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" } diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml index b1e1be49de9..7b16d266982 100644 --- a/app/views/projects/blob/_text.html.haml +++ b/app/views/projects/blob/_text.html.haml @@ -3,7 +3,7 @@ .nothing-here-block File too large, you can = succeed '.' do - = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank' + = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer' - else - blob.load_all_data!(@repository) 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 8853801016b..afe0b5dba45 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -2,14 +2,14 @@ - 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 } - if @conflict .alert.alert-danger 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" + = 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. .file-editor diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index e0ce8cc9601..4c449e040ee 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,7 +1,7 @@ - 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') + = page_specific_javascript_bundle_tag('blob') %h3.page-title New File diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 78720d88e4e..b597c7f7a12 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} @@ -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/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml index 5d9a776da89..a5a9e4d0621 100644 --- a/app/views/projects/buttons/_koding.html.haml +++ b/app/views/projects/buttons/_koding.html.haml @@ -1,3 +1,3 @@ - if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch) - = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do + = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank', rel: 'noopener noreferrer' do Run in IDE (Koding) diff --git a/app/views/projects/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/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml index c8f0b547f80..9007f2c24ba 100644 --- a/app/views/projects/cycle_analytics/_overview.html.haml +++ b/app/views/projects/cycle_analytics/_overview.html.haml @@ -9,7 +9,7 @@ Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. To set up CA, you must first define a production environment by setting up your CI and then deploy to production. %p - %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more + %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: '_blank' } Read more .col-md-6.overview-image %span.overview-icon = custom_icon ('icon_cycle_analytics_overview') diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 2802a4eca7b..82e0d0025ec 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 diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml index 4c8fe1c271b..bf0f1819073 100644 --- a/app/views/projects/environments/_external_url.html.haml +++ b/app/views/projects/environments/_external_url.html.haml @@ -1,3 +1,3 @@ - if environment.external_url && can?(current_user, :read_environment, environment) - = link_to environment.external_url, target: '_blank', class: 'btn external-url' do + = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do = icon('external-link') diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index b8c1782f050..3b45162df52 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -18,7 +18,11 @@ = 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/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/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 c8f097c69da..6682a85ffa6 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -16,7 +16,7 @@ .pull-right - if @merge_request.source_branch_exists? - if koding_enabled? && @repository.koding_yml - = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do + = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank', rel: 'noopener noreferrer' do Run in IDE (Koding) = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do Check out branch 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/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index 93ed4b68e0e..cde0ce08e14 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -49,7 +49,7 @@ %strong Tip: = succeed '.' do You can also checkout merge requests locally by - = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank' + = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer' :javascript $(function(){ 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/new.html.haml b/app/views/projects/new.html.haml index d129da943f8..34a1214a350 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 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..b02fef638ff 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 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/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/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index 3a323d94cc2..2fb88297fb3 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -4,13 +4,13 @@ %ul.list-unstyled.indent-list %li 1. - = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do + = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do Enable custom slash commands = icon('external-link') on your Mattermost installation %li 2. - = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do + = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noopener noreferrer nofollow' do Add a slash command = icon('external-link') in your Mattermost team with these options: diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index a04fd5035a6..2a1b9d4c465 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -4,7 +4,7 @@ %p This service allows users to perform common operations on this project by entering slash commands in Mattermost. - = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do + = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do View documentation = icon('external-link') %p.inline diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 0d973a20d4c..078b7be6865 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -5,7 +5,7 @@ %p This service allows users to perform common operations on this project by entering slash commands in Slack. - = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do + = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do View documentation = icon('external-link') %p.inline @@ -57,7 +57,7 @@ = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label' .col-sm-10.col-xs-12.text-block = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36) - = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank') + = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer') .form-group = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' 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/_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/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/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/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 0b0f2c9cd1a..17107f55a2d 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -8,7 +8,7 @@ .alert.alert-danger Someone edited the #{issuable.class.model_name.human.downcase} the same time you did. Please check out - = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank" + = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer' and make sure your changes will not unintentionally remove theirs .form-group diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 25a4aec0a38..884bd3ca9ca 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 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..059aeebaf34 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.filter-title + = 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/show.html.haml b/app/views/users/show.html.haml index 76cd330e80a..601187455b3 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', @@ -33,7 +33,7 @@ .profile-header .avatar-holder - = link_to avatar_icon(@user, 400), target: '_blank' do + = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: '' .user-info @@ -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? @@ -97,7 +97,8 @@ Snippets %div{ class: container_class } - .user-callout{ 'callout-svg' => custom_icon('icon_customization') } + - if @user == current_user + .user-callout{ 'callout-svg' => custom_icon('icon_customization') } .tab-content #activity.tab-pane .row-content-block.calender-block.white.second-block.hidden-xs |