diff options
author | Alex Braha Stoll <alexbrahastoll@gmail.com> | 2017-01-29 23:58:56 -0200 |
---|---|---|
committer | Alex Braha Stoll <alexbrahastoll@gmail.com> | 2017-01-29 23:58:56 -0200 |
commit | 4c57fa4282afc1679e2891b215174c92bf883c6a (patch) | |
tree | 2500cce3b212a86384f393cb26c6e83e18155490 /app | |
parent | 48417893d7456dc0d46b0a514a2326cc8ce6076f (diff) | |
parent | b525aff665f139cd12ac5a6df78d722427e759cc (diff) | |
download | gitlab-ce-4c57fa4282afc1679e2891b215174c92bf883c6a.tar.gz |
Merge branch 'master' into 23535-folders-in-wiki-repository
Diffstat (limited to 'app')
631 files changed, 9330 insertions, 3377 deletions
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 5a7d823e84c..993f427c9fb 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */ /* global Turbolinks */ (function() { @@ -61,7 +61,5 @@ } return Admin; - })(); - }).call(this); diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index f60f27d1210..b4a8c827d7f 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */ (function() { var Api = { @@ -29,9 +29,9 @@ return $.ajax({ url: url, data: $.extend({ - search: query, - per_page: 20 - }, options), + search: query, + per_page: 20 + }, options), dataType: "json" }).done(function(groups) { return callback(groups); @@ -73,7 +73,7 @@ return $.ajax({ url: url, type: "POST", - data: {'label': data}, + data: { 'label': data }, dataType: "json" }).done(function(label) { return callback(label); diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e43afbb4cc9..4849aab50f4 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -58,6 +58,7 @@ /*= require_directory ./extensions */ /*= require_directory ./lib/utils */ /*= require_directory ./u2f */ +/*= require_directory ./droplab */ /*= require_directory . */ /*= require fuzzaldrin-plus */ /*= require es6-promise.auto */ @@ -83,7 +84,6 @@ var $sidebarGutterToggle = $('.js-sidebar-toggle'); var $flash = $('.flash-container'); var bootstrapBreakpoint = bp.getBreakpointSize(); - var checkInitialSidebarSize; var fitSidebarForSize; // Set the default path for all cookies to GitLab's root directory @@ -245,19 +245,11 @@ return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); } }; - checkInitialSidebarSize = function () { - bootstrapBreakpoint = bp.getBreakpointSize(); - if (bootstrapBreakpoint === 'xs' || 'sm') { - return $document.trigger('breakpoint:change', [bootstrapBreakpoint]); - } - }; $window.off('resize.app').on('resize.app', function () { return fitSidebarForSize(); }); gl.awardsHandler = new AwardsHandler(); - checkInitialSidebarSize(); new Aside(); - // bind sidebar events new gl.Sidebar(); }); diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js index 9417afc2ea7..8438de6cdf1 100644 --- a/app/assets/javascripts/aside.js +++ b/app/assets/javascripts/aside.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */ (function() { this.Aside = (function() { function Aside() { @@ -21,7 +21,5 @@ } return Aside; - })(); - }).call(this); diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js index f45dbe4cbf2..b16a2c0f73a 100644 --- a/app/assets/javascripts/autosave.js +++ b/app/assets/javascripts/autosave.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, quotes, prefer-template, no-var, one-var, no-unused-vars, one-var-declaration-per-line, no-void, consistent-return, no-empty, max-len */ (function() { this.Autosave = (function() { function Autosave(field, key) { @@ -58,7 +58,5 @@ }; return Autosave; - })(); - }).call(this); diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 107a7978a87..629dc267337 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,9 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, brace-style, no-underscore-dangle, no-return-assign, camelcase */ /* global Cookies */ (function() { this.AwardsHandler = (function() { - var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence + var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; // For separating lists produced by ruby's Array#toSentence function AwardsHandler() { this.aliases = gl.emojiAliases(); $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) { @@ -134,7 +134,7 @@ return this.decrementCounter($emojiButton, emoji); } else { counter = $emojiButton.find('.js-counter'); - counter.text(parseInt(counter.text()) + 1); + counter.text(parseInt(counter.text(), 10) + 1); $emojiButton.addClass('active'); this.addYouToUserList(votesBlock, emoji); return this.animateEmoji($emojiButton); @@ -211,10 +211,10 @@ }; AwardsHandler.prototype.toSentence = function(list) { - if(list.length <= 2){ + if (list.length <= 2) { return list.join(' and '); } - else{ + else { return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1]; } }; @@ -339,7 +339,7 @@ if (Cookies.get('frequently_used_emojis')) { frequentlyUsedEmojis = this.getFrequentlyUsedEmojis(); ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>"); - for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) { + for (i = 0, len = frequentlyUsedEmojis.length; i < len; i += 1) { emoji = frequentlyUsedEmojis[i]; $(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul); } @@ -374,7 +374,5 @@ }; return AwardsHandler; - })(); - }).call(this); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index c62a4c5a456..a6bc262b657 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, max-len */ /* global autosize */ /*= require jquery.ba-resize */ @@ -26,5 +26,4 @@ autosize.update($fields); return $fields.css('resize', 'vertical'); }); - }).call(this); diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js index 3998ee9a0a0..6af8f593872 100644 --- a/app/assets/javascripts/behaviors/details_behavior.js +++ b/app/assets/javascripts/behaviors/details_behavior.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, max-len */ (function() { $(function() { $("body").on("click", ".js-details-target", function() { @@ -23,5 +23,4 @@ return e.preventDefault(); }); }); - }).call(this); diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 586f941a6e3..d4895011be7 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, max-len */ // Quick Submit behavior // @@ -74,5 +74,4 @@ return $this.tooltip('hide'); }); }); - }).call(this); diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index 72362988b2e..ccbd6b993cb 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, no-else-return, consistent-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, no-else-return, consistent-return, max-len */ // Requires Input behavior // // When called on a form with input fields with the `required` attribute, the @@ -59,5 +59,4 @@ return hideOrShowHelpBlock($form); }); }); - }).call(this); diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 index 57bd13eecf8..d3455fa3d8c 100644 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 @@ -1,9 +1,8 @@ -/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* eslint-disable no-param-reassign, comma-dangle */ /* global Api */ /*= require blob/template_selector */ ((global) => { - class BlobCiYamlSelector extends gl.TemplateSelector { requestFile(query) { return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this)); @@ -39,5 +38,4 @@ } global.BlobCiYamlSelectors = BlobCiYamlSelectors; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index eab686c45c3..04bfe363929 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,4 +1,4 @@ -/* 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, padded-blocks, max-len */ +/* 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 */ /* global Dropzone */ (function() { @@ -62,7 +62,5 @@ } return BlobFileDropzone; - })(); - }).call(this); diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index 15563e429a0..5fd0857db29 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -1,4 +1,4 @@ -/* 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, padded-blocks */ +/* 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 blob/template_selector */ @@ -19,7 +19,5 @@ }; return BlobGitignoreSelector; - })(gl.TemplateSelector); - }).call(this); diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js index d7f95093688..8236457f0f1 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selectors.js +++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, padded-blocks, max-len */ +/* 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() { @@ -22,7 +22,5 @@ } return BlobGitignoreSelectors; - })(); - }).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index d9c6f65a083..7a14eb160d0 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -1,4 +1,4 @@ -/* 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, padded-blocks */ +/* 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 blob/template_selector */ @@ -24,7 +24,5 @@ }; return BlobLicenseSelector; - })(gl.TemplateSelector); - }).call(this); diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6 index 268640681d4..c5067b0feae 100644 --- a/app/assets/javascripts/blob/blob_license_selectors.js.es6 +++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars, no-param-reassign, padded-blocks */ +/* eslint-disable no-unused-vars, no-param-reassign */ /* global BlobLicenseSelector */ ((global) => { @@ -20,5 +20,4 @@ } global.BlobLicenseSelectors = BlobLicenseSelectors; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6 index 7a1ee9998c8..7e03ec3b391 100644 --- a/app/assets/javascripts/blob/template_selector.js.es6 +++ b/app/assets/javascripts/blob/template_selector.js.es6 @@ -1,101 +1,101 @@ -/* eslint-disable indent, comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, prefer-const, no-param-reassign, space-in-parens, max-len */ +/* 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(); + 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); - } + 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; - } - }); - } + 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()); - } + bindEvents() { + return this.$input.on('keyup blur', (e) => this.onFilenameUpdate()); + } - toggleLabel(item) { - return item.name; - } + 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'); + onFilenameUpdate() { + var filenameMatches; + if (!this.$input.length) { + return; } - - onClick(item, el, e) { - e.preventDefault(); - return this.requestFile(item); + filenameMatches = this.pattern.test(this.$input.val().trim()); + if (!filenameMatches) { + this.wrapper.addClass('hidden'); + return; } + return this.wrapper.removeClass('hidden'); + } - requestFile(item) { - // This `requestFile` method is an abstract method that should - // be added by all subclasses. - } + onClick(item, el, e) { + e.preventDefault(); + return this.requestFile(item); + } - // To be implemented on the extending class - // e.g. - // Api.gitignoreText item.name, @requestFileSuccess.bind(@) - requestFileSuccess(file, { skipFocus } = {}) { - if (!file) return; + requestFile(item) { + // This `requestFile` method is an abstract method that should + // be added by all subclasses. + } - const oldValue = this.editor.getValue(); - let newValue = file.content; + // To be implemented on the extending class + // e.g. + // Api.gitignoreText item.name, @requestFileSuccess.bind(@) + requestFileSuccess(file, { skipFocus } = {}) { + if (!file) return; - this.editor.setValue(newValue, 1); - if (!skipFocus) this.editor.focus(); + const oldValue = this.editor.getValue(); + const newValue = file.content; - if (this.editor instanceof jQuery) { - this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); - } - } + this.editor.setValue(newValue, 1); + if (!skipFocus) this.editor.focus(); - startLoadingSpinner() { - this.dropdownIcon - .addClass('fa-spinner fa-spin') - .removeClass('fa-chevron-down'); + if (this.editor instanceof jQuery) { + this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent); } + } - stopLoadingSpinner() { - this.dropdownIcon - .addClass('fa-chevron-down') - .removeClass('fa-spinner fa-spin'); - } + 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 = {})); + global.TemplateSelector = TemplateSelector; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js index 8c40e36a80a..dfad9b2122b 100644 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, padded-blocks, max-len */ +/* 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 */ @@ -12,5 +12,4 @@ var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language')); new NewCommitForm($('.js-edit-blob-form')); }); - }).call(this); diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index fa43ff611cc..079445e8278 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -1,9 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, 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, padded-blocks, max-len */ +/* 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); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.EditBlob = (function() { function EditBlob(assets_path, ace_mode) { @@ -84,7 +84,5 @@ }; return EditBlob; - })(); - }).call(this); diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 2c9ab61c94d..f9766471780 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren */ +/* eslint-disable one-var, quote-props, comma-dangle, space-before-function-paren */ /* global Vue */ /* global BoardService */ @@ -16,8 +16,8 @@ //= require ./vue_resource_interceptor $(() => { - const $boardApp = document.getElementById('board-app'), - Store = gl.issueBoards.BoardsStore; + const $boardApp = document.getElementById('board-app'); + const Store = gl.issueBoards.BoardsStore; window.gl = window.gl || {}; diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index d1fb0ec48e0..a32881116d5 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, radix */ +/* eslint-disable comma-dangle, space-before-function-paren, one-var */ /* global Vue */ /* global Sortable */ @@ -45,14 +45,28 @@ const issue = this.list.findIssue(this.detailIssue.issue.id); if (issue) { + const offsetLeft = this.$el.offsetLeft; const boardsList = document.querySelectorAll('.boards-list')[0]; - const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth; - const left = boardsList.scrollLeft - this.$el.offsetLeft; + const left = boardsList.scrollLeft - offsetLeft; + let right = (offsetLeft + this.$el.offsetWidth); + + if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { + // -290 here because width of boardsList is animating so therefore + // getting the width here is incorrect + // 290 is the width of the sidebar + right -= (boardsList.offsetWidth - 290); + } else { + right -= boardsList.offsetWidth; + } if (right - boardsList.scrollLeft > 0) { - boardsList.scrollLeft = right; + $(boardsList).animate({ + scrollLeft: right + }, this.sortableOptions.animation); } else if (left > 0) { - boardsList.scrollLeft = this.$el.offsetLeft; + $(boardsList).animate({ + scrollLeft: offsetLeft + }, this.sortableOptions.animation); } } }, @@ -65,7 +79,7 @@ } }, mounted () { - const options = gl.issueBoards.getBoardSortableDefaultOptions({ + this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, group: 'boards', draggable: '.is-draggable', @@ -74,8 +88,8 @@ gl.issueBoards.onEnd(); if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) { - const order = this.sortable.toArray(), - list = Store.findList('id', parseInt(e.item.dataset.id)); + const order = this.sortable.toArray(); + const list = Store.findList('id', parseInt(e.item.dataset.id, 10)); this.$nextTick(() => { Store.moveList(list, order); @@ -84,7 +98,7 @@ } }); - this.sortable = Sortable.create(this.$el.parentNode, options); + this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions); }, }); })(); diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6 index 0a47a22fad2..d76314c1892 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js.es6 +++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, comma-dangle, semi */ +/* eslint-disable space-before-function-paren, comma-dangle */ /* global Vue */ /* global ListLabel */ @@ -15,7 +15,7 @@ new ListLabel({ title: 'To Do', color: '#F0AD4E' }), new ListLabel({ title: 'Doing', color: '#5CB85C' }) ] - } + }; }, methods: { addDefaultLists () { diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 6711930622b..630fe084175 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, space-before-function-paren, max-len, no-plusplus */ +/* eslint-disable comma-dangle, space-before-function-paren, max-len */ /* global Vue */ /* global Sortable */ @@ -43,7 +43,7 @@ issues () { this.$nextTick(() => { if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) { - this.list.page++; + this.list.page += 1; this.list.getIssues(false); } diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 index 3f5cf8420a8..556826a9148 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, indent */ +/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */ (() => { window.gl = window.gl || {}; @@ -32,17 +32,17 @@ }); }, renderRow (label) { - const active = Store.findList('title', label.title), - $li = $('<li />'), - $a = $('<a />', { - class: (active ? `is-active js-board-list-${active.id}` : ''), - text: label.title, - href: '#' - }), - $labelColor = $('<span />', { - class: 'dropdown-label-box', - style: `background-color: ${label.color}` - }); + const active = Store.findList('title', label.title); + const $li = $('<li />'); + const $a = $('<a />', { + class: (active ? `is-active js-board-list-${active.id}` : ''), + text: label.title, + href: '#' + }); + const $labelColor = $('<span />', { + class: 'dropdown-label-box', + style: `background-color: ${label.color}` + }); return $li.append($a.prepend($labelColor)); }, diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 index a5e62ed775d..b6c6d17274f 100644 --- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 +++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars, no-mixed-operators, prefer-const, comma-dangle, semi */ +/* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */ /* global DocumentTouch */ ((w) => { @@ -19,7 +19,8 @@ gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch; gl.issueBoards.getBoardSortableDefaultOptions = (obj) => { - let defaultSortOptions = { + const defaultSortOptions = { + animation: 200, forceFallback: true, fallbackClass: 'is-dragging', fallbackOnBody: true, @@ -30,7 +31,7 @@ scrollSpeed: 20, onStart: gl.issueBoards.onStart, onEnd: gl.issueBoards.onEnd - } + }; Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; }); return defaultSortOptions; diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index cd942c8332b..31531c3ee34 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, space-in-parens, arrow-parens, comma-dangle, max-len */ +/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, arrow-parens, comma-dangle, max-len */ /* global Vue */ /* global ListLabel */ /* global ListMilestone */ @@ -37,12 +37,12 @@ class ListIssue { } findLabel (findLabel) { - return this.labels.filter( label => label.title === findLabel.title )[0]; + return this.labels.filter(label => label.title === findLabel.title)[0]; } removeLabel (removeLabel) { if (removeLabel) { - this.labels = this.labels.filter( label => removeLabel.title !== label.title ); + this.labels = this.labels.filter(label => removeLabel.title !== label.title); } } @@ -51,7 +51,7 @@ class ListIssue { } getLists () { - return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) ); + return gl.issueBoards.BoardsStore.state.lists.filter(list => list.findIssue(this.id)); } update (url) { @@ -60,7 +60,7 @@ class ListIssue { milestone_id: this.milestone ? this.milestone.id : null, due_date: this.dueDate, assignee_id: this.assignee ? this.assignee.id : null, - label_ids: this.labels.map( (label) => label.id ) + label_ids: this.labels.map((label) => label.id) } }; diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index fb13f529b11..3dd5f273057 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-plusplus, prefer-const, space-in-parens, no-shadow, no-param-reassign, max-len, no-unused-vars */ +/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */ /* global ListIssue */ /* global ListLabel */ @@ -58,7 +58,7 @@ class List { nextPage () { if (this.issuesSize > this.issues.length) { - this.page++; + this.page += 1; return this.getIssues(false); } @@ -66,12 +66,12 @@ class List { getIssues (emptyIssues = true) { const filters = this.filters; - let data = { page: this.page }; + const data = { page: this.page }; Object.keys(filters).forEach((key) => { data[key] = filters[key]; }); if (this.label) { - data.label_name = data.label_name.filter( label => label !== this.label.title ); + data.label_name = data.label_name.filter(label => label !== this.label.title); } if (emptyIssues) { @@ -94,7 +94,7 @@ class List { newIssue (issue) { this.addIssue(issue); - this.issuesSize++; + this.issuesSize += 1; return gl.boardService.newIssue(this.id, issue) .then((resp) => { @@ -122,7 +122,7 @@ class List { } if (listFrom) { - this.issuesSize++; + this.issuesSize += 1; gl.boardService.moveIssue(issue.id, listFrom.id, this.id) .then(() => { listFrom.getIssues(false); @@ -132,7 +132,7 @@ class List { } findIssue (id) { - return this.issues.filter( issue => issue.id === id )[0]; + return this.issues.filter(issue => issue.id === id)[0]; } removeIssue (removeIssue) { @@ -140,7 +140,7 @@ class List { const matchesRemove = removeIssue.id === issue.id; if (matchesRemove) { - this.issuesSize--; + this.issuesSize -= 1; issue.removeLabel(this.label); } diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index f18633f0913..ea55158306b 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, prefer-const, no-extra-semi, max-len, no-unused-vars */ +/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, max-len, no-unused-vars */ /* global Vue */ class BoardService { @@ -47,7 +47,7 @@ class BoardService { } getIssuesForList (id, filter = {}) { - let data = { id }; + const data = { id }; Object.keys(filter).forEach((key) => { data[key] = filter[key]; }); return this.issues.get(data); diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index e7a14ea5bca..cdf1b09c0a4 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, space-in-parens, no-shadow, radix, dot-notation, semi, max-len */ +/* eslint-disable comma-dangle, space-before-function-paren, one-var, no-shadow, dot-notation, max-len */ /* global Cookies */ /* global List */ @@ -33,8 +33,8 @@ return list; }, new (listObj) { - const list = this.addList(listObj), - backlogList = this.findList('type', 'backlog', 'backlog'); + const list = this.addList(listObj); + const backlogList = this.findList('type', 'backlog', 'backlog'); list .save() @@ -52,7 +52,7 @@ }, shouldAddBlankState () { // Decide whether to add the blank state - return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]); + return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]); }, addBlankState () { if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; @@ -82,20 +82,20 @@ if (!list) return; - this.state.lists = this.state.lists.filter( list => list.id !== id ); + this.state.lists = this.state.lists.filter(list => list.id !== id); }, moveList (listFrom, orderLists) { orderLists.forEach((id, i) => { - const list = this.findList('id', parseInt(id)); + const list = this.findList('id', parseInt(id, 10)); list.position = i; }); listFrom.update(); }, moveIssueToList (listFrom, listTo, issue, newIndex) { - const issueTo = listTo.findIssue(issue.id), - issueLists = issue.getLists(), - listLabels = issueLists.map( listIssue => listIssue.label ); + const issueTo = listTo.findIssue(issue.id); + const issueLists = issue.getLists(); + const listLabels = issueLists.map(listIssue => listIssue.label); // Add to new lists issues if it doesn't already exist if (!issueTo) { @@ -105,7 +105,7 @@ if (listTo.type === 'done' && listFrom.type !== 'backlog') { issueLists.forEach((list) => { list.removeIssue(issue); - }) + }); issue.removeLabels(listLabels); } else { listFrom.removeIssue(issue); diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js index 01e09ec482e..f05780167bf 100644 --- a/app/assets/javascripts/boards/test_utils/simulate_drag.js +++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js @@ -1,120 +1,119 @@ -/* eslint-disable wrap-iife, func-names, strict, indent, no-tabs, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, object-curly-spacing, no-unused-expressions, prefer-arrow-callback, max-len */ +/* eslint-disable wrap-iife, func-names, strict, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, no-unused-expressions, prefer-arrow-callback, max-len */ (function () { - 'use strict'; - - function simulateEvent(el, type, options) { - var event; - if (!el) return; - var ownerDocument = el.ownerDocument; - - options = options || {}; - - if (/^mouse/.test(type)) { - event = ownerDocument.createEvent('MouseEvents'); - event.initMouseEvent(type, true, true, ownerDocument.defaultView, - options.button, options.screenX, options.screenY, options.clientX, options.clientY, - options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); - } else { - event = ownerDocument.createEvent('CustomEvent'); - - event.initCustomEvent(type, true, true, ownerDocument.defaultView, - options.button, options.screenX, options.screenY, options.clientX, options.clientY, - options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); - - event.dataTransfer = { - data: {}, - - setData: function (type, val) { - this.data[type] = val; - }, - - getData: function (type) { - return this.data[type]; - } - }; - } - - if (el.dispatchEvent) { - el.dispatchEvent(event); - } else if (el.fireEvent) { - el.fireEvent('on' + type, event); - } - - return event; - } - - function getTraget(target) { - var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; - var children = el.children; - - return ( - children[target.index] || - children[target.index === 'first' ? 0 : -1] || - children[target.index === 'last' ? children.length - 1 : -1] - ); - } - - function getRect(el) { - var rect = el.getBoundingClientRect(); - var width = rect.right - rect.left; - var height = rect.bottom - rect.top; - - return { - x: rect.left, - y: rect.top, - cx: rect.left + width / 2, - cy: rect.top + height / 2, - w: width, - h: height, - hw: width / 2, - wh: height / 2 - }; - } - - function simulateDrag(options, callback) { - options.to.el = options.to.el || options.from.el; - - var fromEl = getTraget(options.from); - var toEl = getTraget(options.to); + 'use strict'; + + function simulateEvent(el, type, options) { + var event; + if (!el) return; + var ownerDocument = el.ownerDocument; + + options = options || {}; + + if (/^mouse/.test(type)) { + event = ownerDocument.createEvent('MouseEvents'); + event.initMouseEvent(type, true, true, ownerDocument.defaultView, + options.button, options.screenX, options.screenY, options.clientX, options.clientY, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); + } else { + event = ownerDocument.createEvent('CustomEvent'); + + event.initCustomEvent(type, true, true, ownerDocument.defaultView, + options.button, options.screenX, options.screenY, options.clientX, options.clientY, + options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); + + event.dataTransfer = { + data: {}, + + setData: function (type, val) { + this.data[type] = val; + }, + + getData: function (type) { + return this.data[type]; + } + }; + } + + if (el.dispatchEvent) { + el.dispatchEvent(event); + } else if (el.fireEvent) { + el.fireEvent('on' + type, event); + } + + return event; + } + + function getTraget(target) { + var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; + var children = el.children; + + return ( + children[target.index] || + children[target.index === 'first' ? 0 : -1] || + children[target.index === 'last' ? children.length - 1 : -1] + ); + } + + function getRect(el) { + var rect = el.getBoundingClientRect(); + var width = rect.right - rect.left; + var height = rect.bottom - rect.top; + + return { + x: rect.left, + y: rect.top, + cx: rect.left + width / 2, + cy: rect.top + height / 2, + w: width, + h: height, + hw: width / 2, + wh: height / 2 + }; + } + + function simulateDrag(options, callback) { + options.to.el = options.to.el || options.from.el; + + var fromEl = getTraget(options.from); + var toEl = getTraget(options.to); var scrollable = options.scrollable; - var fromRect = getRect(fromEl); - var toRect = getRect(toEl); - - var startTime = new Date().getTime(); - var duration = options.duration || 1000; - simulateEvent(fromEl, 'mousedown', {button: 0}); - options.ontap && options.ontap(); - window.SIMULATE_DRAG_ACTIVE = 1; - - var dragInterval = setInterval(function loop() { - var progress = (new Date().getTime() - startTime) / duration; - var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft; - var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop; - var overEl = fromEl.ownerDocument.elementFromPoint(x, y); - - simulateEvent(overEl, 'mousemove', { - clientX: x, - clientY: y - }); - - if (progress >= 1) { - options.ondragend && options.ondragend(); - simulateEvent(toEl, 'mouseup'); - clearInterval(dragInterval); - window.SIMULATE_DRAG_ACTIVE = 0; - } - }, 100); - - return { - target: fromEl, - fromList: fromEl.parentNode, - toList: toEl.parentNode - }; - } - - - // Export - window.simulateEvent = simulateEvent; - window.simulateDrag = simulateDrag; + var fromRect = getRect(fromEl); + var toRect = getRect(toEl); + + var startTime = new Date().getTime(); + var duration = options.duration || 1000; + simulateEvent(fromEl, 'mousedown', { button: 0 }); + options.ontap && options.ontap(); + window.SIMULATE_DRAG_ACTIVE = 1; + + var dragInterval = setInterval(function loop() { + var progress = (new Date().getTime() - startTime) / duration; + var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft; + var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop; + var overEl = fromEl.ownerDocument.elementFromPoint(x, y); + + simulateEvent(overEl, 'mousemove', { + clientX: x, + clientY: y + }); + + if (progress >= 1) { + options.ondragend && options.ondragend(); + simulateEvent(toEl, 'mouseup'); + clearInterval(dragInterval); + window.SIMULATE_DRAG_ACTIVE = 0; + } + }, 100); + + return { + target: fromEl, + fromList: fromEl.parentNode, + toList: toEl.parentNode + }; + } + + // Export + window.simulateEvent = simulateEvent; + window.simulateDrag = simulateDrag; })(); diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 index 3723a2039f9..54c2b4ad369 100644 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 @@ -1,10 +1,10 @@ -/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, no-plusplus */ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */ /* global Vue */ Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; next(function (response) { - Vue.activeResources--; + Vue.activeResources -= 1; }); }); diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index a7e72430141..eae062a3aa3 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, no-return-assign, new-parens, no-param-reassign, max-len */ (function() { var Breakpoints = (function() { @@ -52,7 +52,6 @@ }; return BreakpointInstance; - })(); Breakpoints.get = function() { @@ -60,7 +59,6 @@ }; return Breakpoints; - })(); $((function(_this) { diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js index 30432dae278..dbdadc73c3f 100644 --- a/app/assets/javascripts/broadcast_message.js +++ b/app/assets/javascripts/broadcast_message.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */ (function() { $(function() { var previewPath; @@ -31,5 +31,4 @@ } }); }); - }).call(this); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index bc13c46443a..0df84234520 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,10 +1,11 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */ /* global Breakpoints */ /* global Turbolinks */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var AUTO_SCROLL_OFFSET = 75; + var DOWN_BUILD_TRACE = '#down-build-trace'; this.Build = (function() { Build.interval = null; @@ -26,7 +27,7 @@ this.$autoScrollStatus = $('#autoscroll-status'); this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text'); this.$upBuildTrace = $('#up-build-trace'); - this.$downBuildTrace = $('#down-build-trace'); + this.$downBuildTrace = $(DOWN_BUILD_TRACE); this.$scrollTopBtn = $('#scroll-top'); this.$scrollBottomBtn = $('#scroll-bottom'); this.$buildRefreshAnimation = $('.js-build-refresh'); @@ -69,7 +70,7 @@ this.$sidebar = $('.js-build-sidebar'); this.sidebarTranslationLimits = { min: $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() - } + }; this.sidebarTranslationLimits.max = this.sidebarTranslationLimits.min + $('.scrolling-tabs-container').outerHeight(); this.$sidebar.css({ top: this.sidebarTranslationLimits.max @@ -84,13 +85,16 @@ }; Build.prototype.getInitialBuildTrace = function() { - var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] + var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']; return $.ajax({ url: this.buildUrl, dataType: 'json', success: function(buildData) { $('.js-build-output').html(buildData.trace_html); + if (window.location.hash === DOWN_BUILD_TRACE) { + $("html,body").scrollTop(this.$buildTrace.height()); + } if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { this.$buildRefreshAnimation.remove(); return this.initScrollMonitor(); @@ -105,6 +109,8 @@ dataType: "json", success: (function(_this) { return function(log) { + var pageUrl; + if (log.state) { _this.state = log.state; } @@ -116,7 +122,12 @@ } return _this.checkAutoscroll(); } else if (log.status !== _this.buildStatus) { - return Turbolinks.visit(_this.pageUrl); + pageUrl = _this.pageUrl; + if (_this.$autoScrollStatus.data('state') === 'enabled') { + pageUrl += DOWN_BUILD_TRACE; + } + + return Turbolinks.visit(pageUrl); } }; })(this) @@ -142,7 +153,7 @@ this.$scrollTopBtn.hide(); this.$scrollBottomBtn.hide(); this.$autoScrollContainer.hide(); - } + }; // Page scroll listener to detect if user has scrolling page // and handle following cases @@ -280,7 +291,5 @@ }; return Build; - })(); - }).call(this); diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js index c423a548a30..083448552b6 100644 --- a/app/assets/javascripts/build_artifacts.js +++ b/app/assets/javascripts/build_artifacts.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, no-return-assign, max-len */ (function() { this.BuildArtifacts = (function() { function BuildArtifacts() { @@ -22,7 +22,5 @@ }; return BuildArtifacts; - })(); - }).call(this); diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6 index 993424d422f..99082b412e2 100644 --- a/app/assets/javascripts/build_variables.js.es6 +++ b/app/assets/javascripts/build_variables.js.es6 @@ -1,7 +1,7 @@ -/* eslint-disable func-names, prefer-arrow-callback, space-before-blocks, space-before-function-paren, comma-spacing, max-len */ +/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren */ -$(function(){ - $('.reveal-variables').off('click').on('click',function(){ +$(function() { + $('.reveal-variables').off('click').on('click', function() { $('.js-build').toggle().niceScroll(); $(this).hide(); }); diff --git a/app/assets/javascripts/ci_lint_editor.js.es6 b/app/assets/javascripts/ci_lint_editor.js.es6 new file mode 100644 index 00000000000..56ffaa765a8 --- /dev/null +++ b/app/assets/javascripts/ci_lint_editor.js.es6 @@ -0,0 +1,18 @@ +(() => { + window.gl = window.gl || {}; + + class CILintEditor { + constructor() { + this.editor = window.ace.edit('ci-editor'); + this.textarea = document.querySelector('#content'); + + this.editor.getSession().setMode('ace/mode/yaml'); + this.editor.on('input', () => { + const content = this.editor.getSession().getValue(); + this.textarea.value = content; + }); + } + } + + gl.CILintEditor = CILintEditor; +})(); diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js index 67b33a4d7ee..c656ae4e241 100644 --- a/app/assets/javascripts/commit.js +++ b/app/assets/javascripts/commit.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife */ /* global CommitFile */ (function() { @@ -10,7 +10,5 @@ } return Commit; - })(); - }).call(this); diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js index 27512312c7c..184b4561d2e 100644 --- a/app/assets/javascripts/commit/file.js +++ b/app/assets/javascripts/commit/file.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new */ /* global ImageFile */ (function() { @@ -10,7 +10,5 @@ } return CommitFile; - })(); - }).call(this); diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index fd8910e916f..f09a6b1e676 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */ (function() { gl.ImageFile = (function() { var prepareFrames; @@ -172,7 +172,5 @@ }; return ImageFile; - })(); - }).call(this); diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 24a6e4ff0e9..c6fdfbcaa10 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,9 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */ /* global Pager */ (function() { this.CommitsList = (function() { - function CommitsList() {} + var CommitsList = {}; CommitsList.timer = null; @@ -20,6 +20,7 @@ }); this.content = $("#commits-list"); this.searchField = $("#commits-search"); + this.lastSearch = this.searchField.val(); return this.initSearch(); }; @@ -37,6 +38,7 @@ var commitsUrl, form, search; form = $(".commits-search-form"); search = CommitsList.searchField.val(); + if (search === CommitsList.lastSearch) return; commitsUrl = form.attr("action") + '?' + form.serialize(); CommitsList.content.fadeTo('fast', 0.5); return $.ajax({ @@ -47,18 +49,20 @@ return CommitsList.content.fadeTo('fast', 1.0); }, success: function(data) { + CommitsList.lastSearch = search; CommitsList.content.html(data.html); return history.replaceState({ page: commitsUrl // Change url so if user reload a page - search results are saved }, document.title, commitsUrl); }, + error: function() { + CommitsList.lastSearch = null; + }, dataType: "json" }); }; return CommitsList; - })(); - }).call(this); diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index d4243baadb5..9591df70e9c 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ (function() { this.Compare = (function() { function Compare(opts) { @@ -87,7 +87,5 @@ }; return Compare; - })(); - }).call(this); diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6 index 45c974b2b68..3587431ab69 100644 --- a/app/assets/javascripts/compare_autocomplete.js.es6 +++ b/app/assets/javascripts/compare_autocomplete.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, no-dupe-keys, wrap-iife, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ (function() { this.CompareAutocomplete = (function() { @@ -28,7 +28,6 @@ selectable: true, filterable: true, filterByText: true, - toggleLabel: true, fieldName: $dropdown.data('field-name'), filterInput: 'input[type="search"]', renderRow: function(ref) { @@ -55,11 +54,16 @@ $('.dropdown-toggle-text', $dropdown).text(text); $dropdownContainer.removeClass('open'); }); + + $dropdownContainer.on('click', '.dropdown-content a', (e) => { + $dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); + if ($dropdown.hasClass('has-tooltip')) { + $dropdown.tooltip('fixTitle'); + } + }); }); }; return CompareAutocomplete; - })(); - }).call(this); diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index 686a48486f3..35d98492012 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, camelcase, one-var-declaration-per-line, no-else-return, max-len */ (function() { this.ConfirmDangerModal = (function() { function ConfirmDangerModal(form, text) { @@ -27,7 +27,5 @@ } return ConfirmDangerModal; - })(); - }).call(this); diff --git a/app/assets/javascripts/copy_as_gfm.js.es6 b/app/assets/javascripts/copy_as_gfm.js.es6 new file mode 100644 index 00000000000..b94125a4210 --- /dev/null +++ b/app/assets/javascripts/copy_as_gfm.js.es6 @@ -0,0 +1,355 @@ +/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ +/* jshint esversion: 6 */ + +/*= require lib/utils/common_utils */ + +(() => { + const gfmRules = { + // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert + // GitLab Flavored Markdown (GFM) to HTML. + // These handlers consequently convert that same HTML to GFM to be copied to the clipboard. + // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML + // from GFM should have a handler here, in reverse order. + // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb. + InlineDiffFilter: { + 'span.idiff.addition'(el, text) { + return `{+${text}+}`; + }, + 'span.idiff.deletion'(el, text) { + return `{-${text}-}`; + }, + }, + TaskListFilter: { + 'input[type=checkbox].task-list-item-checkbox'(el, text) { + return `[${el.checked ? 'x' : ' '}]`; + }, + }, + ReferenceFilter: { + 'a.gfm:not([data-link=true])'(el, text) { + return el.dataset.original || text; + }, + }, + AutolinkFilter: { + 'a'(el, text) { + // Fallback on the regular MarkdownFilter's `a` handler. + if (text !== el.getAttribute('href')) return false; + + return text; + }, + }, + TableOfContentsFilter: { + 'ul.section-nav'(el, text) { + return '[[_TOC_]]'; + }, + }, + EmojiFilter: { + 'img.emoji'(el, text) { + return el.getAttribute('alt'); + }, + }, + ImageLinkFilter: { + 'a.no-attachment-icon'(el, text) { + return text; + }, + }, + VideoLinkFilter: { + '.video-container'(el, text) { + const videoEl = el.querySelector('video'); + if (!videoEl) return false; + + return CopyAsGFM.nodeToGFM(videoEl); + }, + 'video'(el, text) { + return `![${el.dataset.title}](${el.getAttribute('src')})`; + }, + }, + MathFilter: { + 'pre.code.math[data-math-style=display]'(el, text) { + return `\`\`\`math\n${text.trim()}\n\`\`\``; + }, + 'code.code.math[data-math-style=inline]'(el, text) { + return `$\`${text}\`$`; + }, + 'span.katex-display span.katex-mathml'(el, text) { + const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); + if (!mathAnnotation) return false; + + return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``; + }, + 'span.katex-mathml'(el, text) { + const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); + if (!mathAnnotation) return false; + + return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`; + }, + 'span.katex-html'(el, text) { + // We don't want to include the content of this element in the copied text. + return ''; + }, + 'annotation[encoding="application/x-tex"]'(el, text) { + return text.trim(); + }, + }, + SanitizationFilter: { + 'dl'(el, text) { + let lines = text.trim().split('\n'); + // Add two spaces to the front of subsequent list items lines, + // or leave the line entirely blank. + lines = lines.map((l) => { + const line = l.trim(); + if (line.length === 0) return ''; + + return ` ${line}`; + }); + + return `<dl>\n${lines.join('\n')}\n</dl>`; + }, + 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) { + const tag = el.nodeName.toLowerCase(); + return `<${tag}>${text}</${tag}>`; + }, + }, + SyntaxHighlightFilter: { + 'pre.code.highlight'(el, t) { + const text = t.trim(); + + let lang = el.getAttribute('lang'); + if (lang === 'plaintext') { + lang = ''; + } + + // Prefixes lines with 4 spaces if the code contains triple backticks + if (lang === '' && text.match(/^```/gm)) { + return text.split('\n').map((l) => { + const line = l.trim(); + if (line.length === 0) return ''; + + return ` ${line}`; + }).join('\n'); + } + + return `\`\`\`${lang}\n${text}\n\`\`\``; + }, + 'pre > code'(el, text) { + // Don't wrap code blocks in `` + return text; + }, + }, + MarkdownFilter: { + 'br'(el, text) { + // Two spaces at the end of a line are turned into a BR + return ' '; + }, + 'code'(el, text) { + let backtickCount = 1; + const backtickMatch = text.match(/`+/); + if (backtickMatch) { + backtickCount = backtickMatch[0].length + 1; + } + + const backticks = Array(backtickCount + 1).join('`'); + const spaceOrNoSpace = backtickCount > 1 ? ' ' : ''; + + return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks; + }, + 'blockquote'(el, text) { + return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); + }, + 'img'(el, text) { + return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; + }, + 'a.anchor'(el, text) { + // Don't render a Markdown link for the anchor link inside a heading + return text; + }, + 'a'(el, text) { + return `[${text}](${el.getAttribute('href')})`; + }, + 'li'(el, text) { + const lines = text.trim().split('\n'); + const firstLine = `- ${lines.shift()}`; + // Add four spaces to the front of subsequent list items lines, + // or leave the line entirely blank. + const nextLines = lines.map((s) => { + if (s.trim().length === 0) return ''; + + return ` ${s}`; + }); + + return `${firstLine}\n${nextLines.join('\n')}`; + }, + 'ul'(el, text) { + return text; + }, + 'ol'(el, text) { + // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists. + return text.replace(/^- /mg, '1. '); + }, + 'h1'(el, text) { + return `# ${text.trim()}`; + }, + 'h2'(el, text) { + return `## ${text.trim()}`; + }, + 'h3'(el, text) { + return `### ${text.trim()}`; + }, + 'h4'(el, text) { + return `#### ${text.trim()}`; + }, + 'h5'(el, text) { + return `##### ${text.trim()}`; + }, + 'h6'(el, text) { + return `###### ${text.trim()}`; + }, + 'strong'(el, text) { + return `**${text}**`; + }, + 'em'(el, text) { + return `_${text}_`; + }, + 'del'(el, text) { + return `~~${text}~~`; + }, + 'sup'(el, text) { + return `^${text}`; + }, + 'hr'(el, text) { + return '-----'; + }, + 'table'(el, text) { + const theadEl = el.querySelector('thead'); + const tbodyEl = el.querySelector('tbody'); + if (!theadEl || !tbodyEl) return false; + + const theadText = CopyAsGFM.nodeToGFM(theadEl); + const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl); + + return theadText + tbodyText; + }, + 'thead'(el, text) { + const cells = _.map(el.querySelectorAll('th'), (cell) => { + let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2; + + let before = ''; + let after = ''; + switch (cell.style.textAlign) { + case 'center': + before = ':'; + after = ':'; + chars -= 2; + break; + case 'right': + after = ':'; + chars -= 1; + break; + default: + break; + } + + chars = Math.max(chars, 3); + + const middle = Array(chars + 1).join('-'); + + return before + middle + after; + }); + + return `${text}|${cells.join('|')}|`; + }, + 'tr'(el, text) { + const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim()); + return `| ${cells.join(' | ')} |`; + }, + }, + }; + + class CopyAsGFM { + constructor() { + $(document).on('copy', '.md, .wiki', this.handleCopy); + $(document).on('paste', '.js-gfm-input', this.handlePaste); + } + + handleCopy(e) { + const clipboardData = e.originalEvent.clipboardData; + if (!clipboardData) return; + + const documentFragment = window.gl.utils.getSelectedFragment(); + if (!documentFragment) return; + + // If the documentFragment contains more than just Markdown, don't copy as GFM. + if (documentFragment.querySelector('.md, .wiki')) return; + + e.preventDefault(); + clipboardData.setData('text/plain', documentFragment.textContent); + + const gfm = CopyAsGFM.nodeToGFM(documentFragment); + clipboardData.setData('text/x-gfm', gfm); + } + + handlePaste(e) { + const clipboardData = e.originalEvent.clipboardData; + if (!clipboardData) return; + + const gfm = clipboardData.getData('text/x-gfm'); + if (!gfm) return; + + e.preventDefault(); + + window.gl.utils.insertText(e.target, gfm); + } + + static nodeToGFM(node) { + if (node.nodeType === Node.TEXT_NODE) { + return node.textContent; + } + + const text = this.innerGFM(node); + + if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { + return text; + } + + for (const filter in gfmRules) { + const rules = gfmRules[filter]; + + for (const selector in rules) { + const func = rules[selector]; + + if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; + + const result = func(node, text); + if (result === false) continue; + + return result; + } + } + + return text; + } + + static innerGFM(parentNode) { + const nodes = parentNode.childNodes; + + const clonedParentNode = parentNode.cloneNode(true); + const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0); + + for (let i = 0; i < nodes.length; i += 1) { + const node = nodes[i]; + const clonedNode = clonedNodes[i]; + + const text = this.nodeToGFM(node); + + // `clonedNode.replaceWith(text)` is not yet widely supported + clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); + } + + return clonedParentNode.innerText || clonedParentNode.textContent; + } + } + + window.gl = window.gl || {}; + window.gl.CopyAsGFM = CopyAsGFM; + + new CopyAsGFM(); +})(); diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 6a13f38588d..3485f8f91ed 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, max-len */ /* global Clipboard */ /*= require clipboard */ @@ -46,5 +46,4 @@ clipboard.on('success', genericSuccess); return clipboard.on('error', genericError); }); - }).call(this); diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index 9cf33e62958..35a029194d0 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -1,5 +1,7 @@ /* eslint-disable class-methods-use-this */ +//= require lib/utils/url_utility */ + (() => { const UNFOLD_COUNT = 20; @@ -20,7 +22,7 @@ .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); - this.highlighSelectedLine(); + this.openAnchoredDiff(); } handleClickUnfold(e) { @@ -61,13 +63,22 @@ $.get(link, params, response => $target.parent().replaceWith(response)); } - openAnchoredDiff(anchoredDiff, cb) { - const diffTitle = $(`#file-path-${anchoredDiff}`); + openAnchoredDiff(cb) { + const locationHash = gl.utils.getLocationHash(); + const anchoredDiff = locationHash && locationHash.split('_')[0]; + + if (!anchoredDiff) return; + + const diffTitle = $(`#${anchoredDiff}`); const diffFile = diffTitle.closest('.diff-file'); const nothingHereBlock = $('.nothing-here-block:visible', diffFile); if (nothingHereBlock.length) { - diffFile.singleFileDiff(true, cb); - } else { + const clickTarget = $('.file-title, .click-to-expand', diffFile); + diffFile.data('singleFileDiff').toggleDiff(clickTarget, () => { + this.highlighSelectedLine(); + if (cb) cb(); + }); + } else if (cb) { cb(); } } @@ -95,11 +106,11 @@ } highlighSelectedLine() { + const hash = gl.utils.getLocationHash(); const $diffFiles = $('.diff-file'); $diffFiles.find('.hll').removeClass('hll'); - if (window.location.hash !== '') { - const hash = window.location.hash.replace('#', ''); + if (hash) { $diffFiles .find(`tr#${hash}:not(.match) td, td#${hash}, td[data-line-code="${hash}"]`) .addClass('hll'); diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 index c59d3996fab..2514459e65e 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, semi, max-len */ +/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, max-len */ /* global Vue */ /* global CommentsStore */ @@ -10,7 +10,7 @@ data() { return { textareaIsEmpty: true - } + }; }, computed: { discussion: function () { diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 index f47867fc3b0..c3898873eaa 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, semi */ +/* 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 Vue */ /* global DiscussionMixins */ /* global CommentsStore */ @@ -46,13 +46,13 @@ }, methods: { jumpToNextUnresolvedDiscussion: function () { - let discussionsSelector, - discussionIdsInScope, - firstUnresolvedDiscussionId, - nextUnresolvedDiscussionId, - activeTab = window.mrTabs.currentAction, - hasDiscussionsToJumpTo = true, - jumpToFirstDiscussion = !this.discussionId; + let discussionsSelector; + let discussionIdsInScope; + let firstUnresolvedDiscussionId; + let nextUnresolvedDiscussionId; + let activeTab = window.mrTabs.currentAction; + let hasDiscussionsToJumpTo = true; + let jumpToFirstDiscussion = !this.discussionId; const discussionIdsForElements = function(elements) { return elements.map(function() { @@ -68,11 +68,11 @@ let unresolvedDiscussionCount = 0; - for (let i = 0; i < discussionIdsInScope.length; i++) { + for (let i = 0; i < discussionIdsInScope.length; i += 1) { const discussionId = discussionIdsInScope[i]; const discussion = discussions[discussionId]; if (discussion && !discussion.isResolved()) { - unresolvedDiscussionCount++; + unresolvedDiscussionCount += 1; } } @@ -109,7 +109,7 @@ } let currentDiscussionFound = false; - for (let i = 0; i < discussionIdsInScope.length; i++) { + for (let i = 0; i < discussionIdsInScope.length; i += 1) { const discussionId = discussionIdsInScope[i]; const discussion = discussions[discussionId]; @@ -156,7 +156,7 @@ // If the next discussion is closed, toggle it open. if ($target.find('.js-toggle-content').is(':hidden')) { - $target.find('.js-toggle-button i').trigger('click') + $target.find('.js-toggle-button i').trigger('click'); } } else if (activeTab === 'diffs') { // Resolved discussions are hidden in the diffs tab by default. @@ -170,7 +170,7 @@ // If we are on the diffs tab, we don't scroll to the discussion itself, but to // 4 diff lines above it: the line the discussion was in response to + 3 context let prevEl; - for (let i = 0; i < 4; i++) { + for (let i = 0; i < 4; i += 1) { prevEl = $target.prev(); // If the discussion doesn't have 4 lines above it, we'll have to do with fewer. diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 index 840b5aa922e..1b3a57d0962 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -2,8 +2,6 @@ /* global Vue */ /* global ResolveCount */ -//= require vue -//= require vue-resource //= require_directory ./models //= require_directory ./stores //= require_directory ./services diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 index a9ea18bf82b..3c08c222f46 100644 --- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 +++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-plusplus, no-param-reassign, max-len */ +/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-param-reassign, max-len */ ((w) => { w.DiscussionMixins = { @@ -13,7 +13,7 @@ const discussion = this.discussions[discussionId]; if (discussion.isResolved()) { - resolvedCount++; + resolvedCount += 1; } } @@ -26,7 +26,7 @@ const discussion = this.discussions[discussionId]; if (!discussion.isResolved()) { - unresolvedCount++; + unresolvedCount += 1; } } diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6 index 78c74985f78..a52c476352d 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js.es6 +++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable class-methods-use-this, one-var, indent, camelcase, no-new, comma-dangle, semi, no-param-reassign, max-len */ +/* eslint-disable class-methods-use-this, one-var, camelcase, no-new, comma-dangle, no-param-reassign, max-len */ /* global Vue */ /* global Flash */ /* global CommentsStore */ @@ -32,8 +32,8 @@ } toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) { - const discussion = CommentsStore.state[discussionId], - isResolved = discussion.isResolved(); + const discussion = CommentsStore.state[discussionId]; + const isResolved = discussion.isResolved(); let promise; if (isResolved) { @@ -59,7 +59,7 @@ } else { new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert'); } - }) + }); } resolveAll(projectPath, mergeRequestId, discussionId) { diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6 index 1a7abbc6f75..c80d979b977 100644 --- a/app/assets/javascripts/diff_notes/stores/comments.js.es6 +++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable object-shorthand, func-names, camelcase, prefer-const, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ +/* eslint-disable object-shorthand, func-names, camelcase, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */ /* global Vue */ /* global DiscussionModel */ @@ -41,7 +41,7 @@ } }, unresolvedDiscussionIds: function () { - let ids = []; + const ids = []; for (const discussionId in this.state) { const discussion = this.state[discussionId]; diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 1c1b6cd2dad..529d476ca4e 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* global UsernameValidator */ /* global ActiveTabMemoizer */ /* global ShortcutsNavigation */ @@ -84,6 +84,9 @@ break; case 'projects:merge_requests:index': case 'projects:issues:index': + if (gl.FilteredSearchManager) { + new gl.FilteredSearchManager(); + } Issuable.init(); new gl.IssuableBulkActions({ prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', @@ -184,11 +187,6 @@ new TreeView(); } break; - case 'projects:pipelines:index': - new gl.MiniPipelineGraph({ - container: '.js-pipeline-table', - }); - break; case 'projects:pipelines:builds': case 'projects:pipelines:show': const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; @@ -215,7 +213,9 @@ new gl.Members(); new UsersSelect(); break; - case 'projects:project_members:index': + case 'projects:members:show': + new gl.MemberExpirationDate('.js-access-expiration-date-groups'); + new GroupsSelect(); new gl.MemberExpirationDate(); new gl.Members(); new UsersSelect(); @@ -261,9 +261,8 @@ case 'projects:artifacts:browse': new BuildArtifacts(); break; - case 'projects:group_links:index': - new gl.MemberExpirationDate(); - new GroupsSelect(); + case 'help:index': + gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge')); break; case 'search:show': new Search(); @@ -275,6 +274,10 @@ case 'projects:variables:index': new gl.ProjectVariables(); break; + case 'ci:lints:create': + case 'ci:lints:show': + new gl.CILintEditor(); + break; } switch (path.first()) { case 'admin': @@ -372,7 +375,5 @@ }; return Dispatcher; - })(); - }).call(this); diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js new file mode 100644 index 00000000000..8b14191395b --- /dev/null +++ b/app/assets/javascripts/droplab/droplab.js @@ -0,0 +1,741 @@ +/* eslint-disable */ +// Determine where to place this +if (typeof Object.assign != 'function') { + Object.assign = function (target, varArgs) { // .length of function is 2 + 'use strict'; + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }; +} + +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.droplab = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +var DATA_TRIGGER = 'data-dropdown-trigger'; +var DATA_DROPDOWN = 'data-dropdown'; + +module.exports = { + DATA_TRIGGER: DATA_TRIGGER, + DATA_DROPDOWN: DATA_DROPDOWN, +} + +},{}],2:[function(require,module,exports){ +// Custom event support for IE +if ( typeof CustomEvent === "function" ) { + module.exports = CustomEvent; +} else { + require('./window')(function(w){ + var CustomEvent = function ( event, params ) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + var evt = document.createEvent( 'CustomEvent' ); + evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); + return evt; + } + CustomEvent.prototype = w.Event.prototype; + + w.CustomEvent = CustomEvent; + }); + module.exports = CustomEvent; +} + +},{"./window":11}],3:[function(require,module,exports){ +var CustomEvent = require('./custom_event_polyfill'); +var utils = require('./utils'); + +var DropDown = function(list) { + this.currentIndex = 0; + this.hidden = true; + this.list = list; + this.items = []; + this.getItems(); + this.initTemplateString(); + this.addEvents(); + this.initialState = list.innerHTML; +}; + +Object.assign(DropDown.prototype, { + getItems: function() { + this.items = [].slice.call(this.list.querySelectorAll('li')); + return this.items; + }, + + initTemplateString: function() { + var items = this.items || this.getItems(); + + var templateString = ''; + if(items.length > 0) { + templateString = items[items.length - 1].outerHTML; + } + this.templateString = templateString; + return this.templateString; + }, + + clickEvent: function(e) { + // climb up the tree to find the LI + var selected = utils.closest(e.target, 'LI'); + + if(selected) { + e.preventDefault(); + this.hide(); + var listEvent = new CustomEvent('click.dl', { + detail: { + list: this, + selected: selected, + data: e.target.dataset, + }, + }); + this.list.dispatchEvent(listEvent); + } + }, + + addEvents: function() { + this.clickWrapper = this.clickEvent.bind(this); + // event delegation. + this.list.addEventListener('click', this.clickWrapper); + }, + + toggle: function() { + if(this.hidden) { + this.show(); + } else { + this.hide(); + } + }, + + setData: function(data) { + this.data = data; + this.render(data); + }, + + addData: function(data) { + this.data = (this.data || []).concat(data); + this.render(this.data); + }, + + // call render manually on data; + render: function(data){ + // debugger + // empty the list first + var templateString = this.templateString; + var newChildren = []; + var toAppend; + + newChildren = (data ||[]).map(function(dat){ + var html = utils.t(templateString, dat); + var template = document.createElement('div'); + template.innerHTML = html; + + // Help set the image src template + var imageTags = template.querySelectorAll('img[data-src]'); + // debugger + for(var i = 0; i < imageTags.length; i++) { + var imageTag = imageTags[i]; + imageTag.src = imageTag.getAttribute('data-src'); + imageTag.removeAttribute('data-src'); + } + + if(dat.hasOwnProperty('droplab_hidden') && dat.droplab_hidden){ + template.firstChild.style.display = 'none' + }else{ + template.firstChild.style.display = 'block'; + } + return template.firstChild.outerHTML; + }); + toAppend = this.list.querySelector('ul[data-dynamic]'); + if(toAppend) { + toAppend.innerHTML = newChildren.join(''); + } else { + this.list.innerHTML = newChildren.join(''); + } + }, + + show: function() { + if (this.hidden) { + // debugger + this.list.style.display = 'block'; + this.currentIndex = 0; + this.hidden = false; + } + }, + + hide: function() { + if (!this.hidden) { + // debugger + this.list.style.display = 'none'; + this.currentIndex = 0; + this.hidden = true; + } + }, + + destroy: function() { + this.hide(); + this.list.removeEventListener('click', this.clickWrapper); + } +}); + +module.exports = DropDown; + +},{"./custom_event_polyfill":2,"./utils":10}],4:[function(require,module,exports){ +require('./window')(function(w){ + module.exports = function(deps) { + deps = deps || {}; + var window = deps.window || w; + var document = deps.document || window.document; + var CustomEvent = deps.CustomEvent || require('./custom_event_polyfill'); + var HookButton = deps.HookButton || require('./hook_button'); + var HookInput = deps.HookInput || require('./hook_input'); + var utils = deps.utils || require('./utils'); + var DATA_TRIGGER = require('./constants').DATA_TRIGGER; + + var DropLab = function(hook){ + if (!(this instanceof DropLab)) return new DropLab(hook); + this.ready = false; + this.hooks = []; + this.queuedData = []; + this.config = {}; + this.loadWrapper; + if(typeof hook !== 'undefined'){ + this.addHook(hook); + } + }; + + + Object.assign(DropLab.prototype, { + load: function() { + this.loadWrapper(); + }, + + loadWrapper: function(){ + var dropdownTriggers = [].slice.apply(document.querySelectorAll('['+DATA_TRIGGER+']')); + this.addHooks(dropdownTriggers).init(); + }, + + addData: function () { + var args = [].slice.apply(arguments); + this.applyArgs(args, '_addData'); + }, + + setData: function() { + var args = [].slice.apply(arguments); + this.applyArgs(args, '_setData'); + }, + + destroy: function() { + for(var i = 0; i < this.hooks.length; i++) { + this.hooks[i].destroy(); + } + this.hooks = []; + this.removeEvents(); + }, + + applyArgs: function(args, methodName) { + if(this.ready) { + this[methodName].apply(this, args); + } else { + this.queuedData = this.queuedData || []; + this.queuedData.push(args); + } + }, + + _addData: function(trigger, data) { + this._processData(trigger, data, 'addData'); + }, + + _setData: function(trigger, data) { + this._processData(trigger, data, 'setData'); + }, + + _processData: function(trigger, data, methodName) { + for(var i = 0; i < this.hooks.length; i++) { + var hook = this.hooks[i]; + if(hook.trigger.dataset.hasOwnProperty('id')) { + if(hook.trigger.dataset.id === trigger) { + hook.list[methodName](data); + } + } + } + }, + + addEvents: function() { + var self = this; + this.windowClickedWrapper = function(e){ + var thisTag = e.target; + if(thisTag.tagName !== 'UL'){ + // climb up the tree to find the UL + thisTag = utils.closest(thisTag, 'UL'); + } + if(utils.isDropDownParts(thisTag)){ return } + if(utils.isDropDownParts(e.target)){ return } + for(var i = 0; i < self.hooks.length; i++) { + self.hooks[i].list.hide(); + } + }.bind(this); + document.addEventListener('click', this.windowClickedWrapper); + }, + + removeEvents: function(){ + w.removeEventListener('click', this.windowClickedWrapper); + w.removeEventListener('load', this.loadWrapper); + }, + + changeHookList: function(trigger, list, plugins, config) { + trigger = document.querySelector('[data-id="'+trigger+'"]'); + // list = document.querySelector(list); + this.hooks.every(function(hook, i) { + if(hook.trigger === trigger) { + hook.destroy(); + this.hooks.splice(i, 1); + this.addHook(trigger, list, plugins, config); + return false; + } + return true + }.bind(this)); + }, + + addHook: function(hook, list, plugins, config) { + if(!(hook instanceof HTMLElement) && typeof hook === 'string'){ + hook = document.querySelector(hook); + } + if(!list){ + list = document.querySelector(hook.dataset[utils.toDataCamelCase(DATA_TRIGGER)]); + } + + if(hook) { + if(hook.tagName === 'A' || hook.tagName === 'BUTTON') { + this.hooks.push(new HookButton(hook, list, plugins, config)); + } else if(hook.tagName === 'INPUT') { + this.hooks.push(new HookInput(hook, list, plugins, config)); + } + } + return this; + }, + + addHooks: function(hooks, plugins, config) { + for(var i = 0; i < hooks.length; i++) { + var hook = hooks[i]; + this.addHook(hook, null, plugins, config); + } + return this; + }, + + setConfig: function(obj){ + this.config = obj; + }, + + init: function () { + this.addEvents(); + var readyEvent = new CustomEvent('ready.dl', { + detail: { + dropdown: this, + }, + }); + window.dispatchEvent(readyEvent); + this.ready = true; + for(var i = 0; i < this.queuedData.length; i++) { + this.addData.apply(this, this.queuedData[i]); + } + this.queuedData = []; + return this; + }, + }); + + return DropLab; + }; +}); + +},{"./constants":1,"./custom_event_polyfill":2,"./hook_button":6,"./hook_input":7,"./utils":10,"./window":11}],5:[function(require,module,exports){ +var DropDown = require('./dropdown'); + +var Hook = function(trigger, list, plugins, config){ + this.trigger = trigger; + this.list = new DropDown(list); + this.type = 'Hook'; + this.event = 'click'; + this.plugins = plugins || []; + this.config = config || {}; + this.id = trigger.dataset.id; +}; + +Object.assign(Hook.prototype, { + + addEvents: function(){}, + + constructor: Hook, +}); + +module.exports = Hook; + +},{"./dropdown":3}],6:[function(require,module,exports){ +var CustomEvent = require('./custom_event_polyfill'); +var Hook = require('./hook'); + +var HookButton = function(trigger, list, plugins, config) { + Hook.call(this, trigger, list, plugins, config); + this.type = 'button'; + this.event = 'click'; + this.addEvents(); + this.addPlugins(); +}; + +HookButton.prototype = Object.create(Hook.prototype); + +Object.assign(HookButton.prototype, { + addPlugins: function() { + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].init(this); + } + }, + + clicked: function(e){ + var buttonEvent = new CustomEvent('click.dl', { + detail: { + hook: this, + }, + bubbles: true, + cancelable: true + }); + this.list.show(); + e.target.dispatchEvent(buttonEvent); + }, + + addEvents: function(){ + this.clickedWrapper = this.clicked.bind(this); + this.trigger.addEventListener('click', this.clickedWrapper); + }, + + removeEvents: function(){ + this.trigger.removeEventListener('click', this.clickedWrapper); + }, + + restoreInitialState: function() { + this.list.list.innerHTML = this.list.initialState; + }, + + removePlugins: function() { + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].destroy(); + } + }, + + destroy: function() { + this.restoreInitialState(); + this.removeEvents(); + this.removePlugins(); + }, + + + constructor: HookButton, +}); + + +module.exports = HookButton; + +},{"./custom_event_polyfill":2,"./hook":5}],7:[function(require,module,exports){ +var CustomEvent = require('./custom_event_polyfill'); +var Hook = require('./hook'); + +var HookInput = function(trigger, list, plugins, config) { + Hook.call(this, trigger, list, plugins, config); + this.type = 'input'; + this.event = 'input'; + this.addPlugins(); + this.addEvents(); +}; + +Object.assign(HookInput.prototype, { + addPlugins: function() { + var self = this; + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].init(self); + } + }, + + addEvents: function(){ + var self = this; + + this.mousedown = function mousedown(e) { + if(self.hasRemovedEvents) return; + + var mouseEvent = new CustomEvent('mousedown.dl', { + detail: { + hook: self, + text: e.target.value, + }, + bubbles: true, + cancelable: true + }); + e.target.dispatchEvent(mouseEvent); + } + + this.input = function input(e) { + if(self.hasRemovedEvents) return; + + self.list.show(); + + var inputEvent = new CustomEvent('input.dl', { + detail: { + hook: self, + text: e.target.value, + }, + bubbles: true, + cancelable: true + }); + e.target.dispatchEvent(inputEvent); + } + + this.keyup = function keyup(e) { + if(self.hasRemovedEvents) return; + + keyEvent(e, 'keyup.dl'); + } + + this.keydown = function keydown(e) { + if(self.hasRemovedEvents) return; + + keyEvent(e, 'keydown.dl'); + } + + function keyEvent(e, keyEventName){ + self.list.show(); + + var keyEvent = new CustomEvent(keyEventName, { + detail: { + hook: self, + text: e.target.value, + which: e.which, + key: e.key, + }, + bubbles: true, + cancelable: true + }); + e.target.dispatchEvent(keyEvent); + } + + this.events = this.events || {}; + this.events.mousedown = this.mousedown; + this.events.input = this.input; + this.events.keyup = this.keyup; + this.events.keydown = this.keydown; + this.trigger.addEventListener('mousedown', this.mousedown); + this.trigger.addEventListener('input', this.input); + this.trigger.addEventListener('keyup', this.keyup); + this.trigger.addEventListener('keydown', this.keydown); + }, + + removeEvents: function() { + this.hasRemovedEvents = true; + this.trigger.removeEventListener('mousedown', this.mousedown); + this.trigger.removeEventListener('input', this.input); + this.trigger.removeEventListener('keyup', this.keyup); + this.trigger.removeEventListener('keydown', this.keydown); + }, + + restoreInitialState: function() { + this.list.list.innerHTML = this.list.initialState; + }, + + removePlugins: function() { + for(var i = 0; i < this.plugins.length; i++) { + this.plugins[i].destroy(); + } + }, + + destroy: function() { + this.restoreInitialState(); + this.removeEvents(); + this.removePlugins(); + this.list.destroy(); + } +}); + +module.exports = HookInput; + +},{"./custom_event_polyfill":2,"./hook":5}],8:[function(require,module,exports){ +var DropLab = require('./droplab')(); +var DATA_TRIGGER = require('./constants').DATA_TRIGGER; +var keyboard = require('./keyboard')(); +var setup = function() { + window.DropLab = DropLab; +}; + + +module.exports = setup(); + +},{"./constants":1,"./droplab":4,"./keyboard":9}],9:[function(require,module,exports){ +require('./window')(function(w){ + module.exports = function(){ + var currentKey; + var currentFocus; + var isUpArrow = false; + var isDownArrow = false; + var removeHighlight = function removeHighlight(list) { + var listItems = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0); + var listItemsTmp = []; + for(var i = 0; i < listItems.length; i++) { + var listItem = listItems[i]; + listItem.classList.remove('dropdown-active'); + + if (listItem.style.display !== 'none') { + listItemsTmp.push(listItem); + } + } + return listItemsTmp; + }; + + var setMenuForArrows = function setMenuForArrows(list) { + var listItems = removeHighlight(list); + if(list.currentIndex>0){ + if(!listItems[list.currentIndex-1]){ + list.currentIndex = list.currentIndex-1; + } + + if (listItems[list.currentIndex-1]) { + var el = listItems[list.currentIndex-1]; + var filterDropdownEl = el.closest('.filter-dropdown'); + el.classList.add('dropdown-active'); + + if (filterDropdownEl) { + var filterDropdownBottom = filterDropdownEl.offsetHeight; + var elOffsetTop = el.offsetTop - 30; + + if (elOffsetTop > filterDropdownBottom) { + filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom; + } + } + } + } + }; + + var mousedown = function mousedown(e) { + var list = e.detail.hook.list; + removeHighlight(list); + list.show(); + list.currentIndex = 0; + isUpArrow = false; + isDownArrow = false; + }; + var selectItem = function selectItem(list) { + var listItems = removeHighlight(list); + var currentItem = listItems[list.currentIndex-1]; + var listEvent = new CustomEvent('click.dl', { + detail: { + list: list, + selected: currentItem, + data: currentItem.dataset, + }, + }); + list.list.dispatchEvent(listEvent); + list.hide(); + } + + var keydown = function keydown(e){ + var typedOn = e.target; + var list = e.detail.hook.list; + var currentIndex = list.currentIndex; + isUpArrow = false; + isDownArrow = false; + + if(e.detail.which){ + currentKey = e.detail.which; + if(currentKey === 13){ + selectItem(e.detail.hook.list); + return; + } + if(currentKey === 38) { + isUpArrow = true; + } + if(currentKey === 40) { + isDownArrow = true; + } + } else if(e.detail.key) { + currentKey = e.detail.key; + if(currentKey === 'Enter'){ + selectItem(e.detail.hook.list); + return; + } + if(currentKey === 'ArrowUp') { + isUpArrow = true; + } + if(currentKey === 'ArrowDown') { + isDownArrow = true; + } + } + if(isUpArrow){ currentIndex--; } + if(isDownArrow){ currentIndex++; } + if(currentIndex < 0){ currentIndex = 0; } + list.currentIndex = currentIndex; + setMenuForArrows(e.detail.hook.list); + }; + + w.addEventListener('mousedown.dl', mousedown); + w.addEventListener('keydown.dl', keydown); + }; +}); +},{"./window":11}],10:[function(require,module,exports){ +var DATA_TRIGGER = require('./constants').DATA_TRIGGER; +var DATA_DROPDOWN = require('./constants').DATA_DROPDOWN; + +var toDataCamelCase = function(attr){ + return this.camelize(attr.split('-').slice(1).join(' ')); +}; + +// the tiniest damn templating I can do +var t = function(s,d){ + for(var p in d) + s=s.replace(new RegExp('{{'+p+'}}','g'), d[p]); + return s; +}; + +var camelize = function(str) { + return str.replace(/(?:^\w|[A-Z]|\b\w)/g, function(letter, index) { + return index == 0 ? letter.toLowerCase() : letter.toUpperCase(); + }).replace(/\s+/g, ''); +}; + +var closest = function(thisTag, stopTag) { + while(thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML'){ + thisTag = thisTag.parentNode; + } + return thisTag; +}; + +var isDropDownParts = function(target) { + if(!target || target.tagName === 'HTML') { return false; } + return ( + target.hasAttribute(DATA_TRIGGER) || + target.hasAttribute(DATA_DROPDOWN) + ); +}; + +module.exports = { + toDataCamelCase: toDataCamelCase, + t: t, + camelize: camelize, + closest: closest, + isDropDownParts: isDropDownParts, +}; + +},{"./constants":1}],11:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[8])(8) +}); diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js new file mode 100644 index 00000000000..c290e1a8355 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_ajax.js @@ -0,0 +1,96 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* global droplab */ + +require('../window')(function(w){ + function droplabAjaxException(message) { + this.message = message; + } + + w.droplabAjax = { + _loadUrlData: function _loadUrlData(url) { + var self = this; + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest; + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if(xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + self.cache[url] = data; + return resolve(data); + } else { + return reject([xhr.responseText, xhr.status]); + } + } + }; + xhr.send(); + }); + }, + + _loadData: function _loadData(data, config, self) { + if (config.loadingTemplate) { + var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]'); + + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } + } + + self.hook.list[config.method].call(self.hook.list, data); + }, + + init: function init(hook) { + var self = this; + self.cache = self.cache || {}; + var config = hook.config.droplabAjax; + this.hook = hook; + + if (!config || !config.endpoint || !config.method) { + return; + } + + if (config.method !== 'setData' && config.method !== 'addData') { + return; + } + + if (config.loadingTemplate) { + var dynamicList = hook.list.list.querySelector('[data-dynamic]'); + + var loadingTemplate = document.createElement('div'); + loadingTemplate.innerHTML = config.loadingTemplate; + loadingTemplate.setAttribute('data-loading-template', ''); + + this.listTemplate = dynamicList.outerHTML; + dynamicList.outerHTML = loadingTemplate.outerHTML; + } + + if (self.cache[config.endpoint]) { + self._loadData(self.cache[config.endpoint], config, self); + } else { + this._loadUrlData(config.endpoint) + .then(function(d) { + self._loadData(d, config, self); + }).catch(function(e) { + throw new droplabAjaxException(e.message || e); + }); + } + }, + + destroy: function() { + if (this.listTemplate) { + var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); + dynamicList.outerHTML = this.listTemplate; + } + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +}); diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js new file mode 100644 index 00000000000..b63d73066cb --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js @@ -0,0 +1,161 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* global droplab */ + +require('../window')(function(w){ + w.droplabAjaxFilter = { + init: function(hook) { + this.destroyed = false; + this.hook = hook; + this.notLoading(); + + this.debounceTriggerWrapper = this.debounceTrigger.bind(this); + this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper); + this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper); + this.trigger(true); + }, + + notLoading: function notLoading() { + this.loading = false; + }, + + debounceTrigger: function debounceTrigger(e) { + var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93]; + var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1; + var focusEvent = e.type === 'focus'; + + if (invalidKeyPressed || this.loading) { + return; + } + + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200); + }, + + trigger: function trigger(getEntireList) { + var config = this.hook.config.droplabAjaxFilter; + var searchValue = this.trigger.value; + + if (!config || !config.endpoint || !config.searchKey) { + return; + } + + if (config.searchValueFunction) { + searchValue = config.searchValueFunction(); + } + + if (config.loadingTemplate && this.hook.list.data === undefined || + this.hook.list.data.length === 0) { + var dynamicList = this.hook.list.list.querySelector('[data-dynamic]'); + + var loadingTemplate = document.createElement('div'); + loadingTemplate.innerHTML = config.loadingTemplate; + loadingTemplate.setAttribute('data-loading-template', true); + + this.listTemplate = dynamicList.outerHTML; + dynamicList.outerHTML = loadingTemplate.outerHTML; + } + + if (getEntireList) { + searchValue = ''; + } + + if (config.searchKey === searchValue) { + return this.list.show(); + } + + this.loading = true; + + var params = config.params || {}; + params[config.searchKey] = searchValue; + var self = this; + self.cache = self.cache || {}; + var url = config.endpoint + this.buildParams(params); + var urlCachedData = self.cache[url]; + + if (urlCachedData) { + self._loadData(urlCachedData, config, self); + } else { + this._loadUrlData(url) + .then(function(data) { + self._loadData(data, config, self); + }); + } + }, + + _loadUrlData: function _loadUrlData(url) { + var self = this; + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest; + xhr.open('GET', url, true); + xhr.onreadystatechange = function () { + if(xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + self.cache[url] = data; + return resolve(data); + } else { + return reject([xhr.responseText, xhr.status]); + } + } + }; + xhr.send(); + }); + }, + + _loadData: function _loadData(data, config, self) { + if (config.loadingTemplate && self.hook.list.data === undefined || + self.hook.list.data.length === 0) { + const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]'); + + if (dataLoadingTemplate) { + dataLoadingTemplate.outerHTML = self.listTemplate; + } + } + + if (!self.destroyed) { + var hookListChildren = self.hook.list.list.children; + var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); + + if (onlyDynamicList && data.length === 0) { + self.hook.list.hide(); + } + + self.hook.list.setData.call(self.hook.list, data); + } + self.notLoading(); + self.hook.list.currentIndex = 0; + }, + + buildParams: function(params) { + if (!params) return ''; + var paramsArray = Object.keys(params).map(function(param) { + return param + '=' + (params[param] || ''); + }); + return '?' + paramsArray.join('&'); + }, + + destroy: function destroy() { + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.destroyed = true; + + this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper); + this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper); + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +}); diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js new file mode 100644 index 00000000000..9b40a3f20a4 --- /dev/null +++ b/app/assets/javascripts/droplab/droplab_filter.js @@ -0,0 +1,74 @@ +/* eslint-disable */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ +/* global droplab */ + +require('../window')(function(w){ + w.droplabFilter = { + + keydownWrapper: function(e){ + var hiddenCount = 0; + var dataHiddenCount = 0; + var list = e.detail.hook.list; + var data = list.data; + var value = e.detail.hook.trigger.value.toLowerCase(); + var config = e.detail.hook.config.droplabFilter; + var matches = []; + var filterFunction; + // will only work on dynamically set data + if(!data){ + return; + } + + if (config && config.filterFunction && typeof config.filterFunction === 'function') { + filterFunction = config.filterFunction; + } else { + filterFunction = function(o){ + // cheap string search + o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1; + return o; + }; + } + + dataHiddenCount = data.filter(function(o) { + return !o.droplab_hidden; + }).length; + + matches = data.map(function(o) { + return filterFunction(o, value); + }); + + hiddenCount = matches.filter(function(o) { + return !o.droplab_hidden; + }).length; + + if (dataHiddenCount !== hiddenCount) { + list.render(matches); + list.currentIndex = 0; + } + }, + + init: function init(hookInput) { + var config = hookInput.config.droplabFilter; + + if (!config || (!config.template && !config.filterFunction)) { + return; + } + + this.hookInput = hookInput; + this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper); + }, + + destroy: function destroy(){ + this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper); + } + }; +}); +},{"../window":2}],2:[function(require,module,exports){ +module.exports = function(callback) { + return (function() { + callback(this); + }).call(null); +}; + +},{}]},{},[1])(1) +}); diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 56cb39be642..3d183f4ecb4 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */ /* global Dropzone */ /*= require preview_markdown */ @@ -120,7 +120,7 @@ if (item.type.indexOf("image") !== -1) { return item; } - i++; + i += 1; } return false; }; @@ -215,7 +215,5 @@ } return DropzoneInput; - })(); - }).call(this); diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6 index 2b7d57d86c6..d81d4cf8425 100644 --- a/app/assets/javascripts/due_date_select.js.es6 +++ b/app/assets/javascripts/due_date_select.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, prefer-const, padded-blocks, no-unused-vars, no-underscore-dangle, no-new, max-len, semi, no-sequences, no-unused-expressions, no-param-reassign */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, no-unused-vars, no-underscore-dangle, no-new, max-len, no-sequences, no-unused-expressions, no-param-reassign */ (function(global) { class DueDateSelect { @@ -16,7 +16,7 @@ this.$sidebarValue = $('.js-due-date-sidebar-value', $block); this.fieldName = $dropdown.data('field-name'), this.abilityName = $dropdown.data('ability-name'), - this.issueUpdateURL = $dropdown.data('issue-update') + this.issueUpdateURL = $dropdown.data('issue-update'); this.rawSelectedDate = null; this.displayedDate = null; @@ -80,9 +80,12 @@ } parseSelectedDate() { - this.rawSelectedDate = $("input[name='" + this.fieldName + "']").val(); + this.rawSelectedDate = $(`input[name='${this.fieldName}']`).val(); + if (this.rawSelectedDate.length) { - let dateObj = new Date(this.rawSelectedDate); + // Construct Date object manually to avoid buggy dateString support within Date constructor + const dateArray = this.rawSelectedDate.split('-').map(v => parseInt(v, 10)); + const dateObj = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]); this.displayedDate = $.datepicker.formatDate('M d, yy', dateObj); } else { this.displayedDate = 'No due date'; @@ -132,7 +135,6 @@ return selectedDateValue.length ? $('.js-remove-due-date-holder').removeClass('hidden') : $('.js-remove-due-date-holder').addClass('hidden'); - } }).done((data) => { if (isDropdown) { @@ -176,5 +178,4 @@ } global.DueDateSelectors = DueDateSelectors; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index facd653fd72..fea642467fa 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -1,6 +1,7 @@ -/* eslint-disable no-param-reassign */ +/* eslint-disable no-param-reassign, no-new */ /* global Vue */ /* global EnvironmentsService */ +/* global Flash */ //= require vue //= require vue-resource @@ -10,42 +11,7 @@ (() => { window.gl = window.gl || {}; - /** - * Given the visibility prop provided by the url query parameter and which - * changes according to the active tab we need to filter which environments - * should be visible. - * - * The environments array is a recursive tree structure and we need to filter - * both root level environments and children environments. - * - * In order to acomplish that, both `filterState` and `filterEnvironmentsByState` - * functions work together. - * The first one works as the filter that verifies if the given environment matches - * the given state. - * The second guarantees both root level and children elements are filtered as well. - */ - - const filterState = state => environment => environment.state === state && environment; - /** - * Given the filter function and the array of environments will return only - * the environments that match the state provided to the filter function. - * - * @param {Function} fn - * @param {Array} array - * @return {Array} - */ - const filterEnvironmentsByState = (fn, arr) => arr.map((item) => { - if (item.children) { - const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean); - if (filteredChildren.length) { - item.children = filteredChildren; - return item; - } - } - return fn(item); - }).filter(Boolean); - - window.gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { + gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { props: { store: { type: Object, @@ -55,7 +21,7 @@ }, components: { - 'environment-item': window.gl.environmentsList.EnvironmentItem, + 'environment-item': gl.environmentsList.EnvironmentItem, }, data() { @@ -81,10 +47,6 @@ }, computed: { - filteredEnvironments() { - return filterEnvironmentsByState(filterState(this.visibility), this.state.environments); - }, - scope() { return this.$options.getQueryParameter('scope'); }, @@ -111,7 +73,7 @@ const scope = this.$options.getQueryParameter('scope'); if (scope) { - this.visibility = scope; + this.store.storeVisibility(scope); } this.isLoading = true; @@ -121,6 +83,10 @@ .then((json) => { this.store.storeEnvironments(json); this.isLoading = false; + }) + .catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the environments.', 'alert'); }); }, @@ -188,7 +154,7 @@ <div class="blank-state blank-state-no-icon" v-if="!isLoading && state.environments.length === 0"> - <h2 class="blank-state-title"> + <h2 class="blank-state-title js-blank-state-title"> You don't have any environments right now. </h2> <p class="blank-state-text"> @@ -202,13 +168,13 @@ <a v-if="canCreateEnvironmentParsed" :href="newEnvironmentPath" - class="btn btn-create"> + class="btn btn-create js-new-environment-button"> New Environment </a> </div> <div class="table-holder" - v-if="!isLoading && state.environments.length > 0"> + v-if="!isLoading && state.filteredEnvironments.length > 0"> <table class="table ci-table environments"> <thead> <tr> @@ -216,12 +182,12 @@ <th class="environments-deploy">Last deployment</th> <th class="environments-build">Build</th> <th class="environments-commit">Commit</th> - <th class="environments-date"></th> + <th class="environments-date">Created</th> <th class="hidden-xs environments-actions"></th> </tr> </thead> <tbody> - <template v-for="model in filteredEnvironments" + <template v-for="model in state.filteredEnvironments" v-bind:model="model"> <tr diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index 7c743705d51..81468f4d3bc 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -5,7 +5,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.ActionsComponent = Vue.component('actions-component', { + gl.environmentsList.ActionsComponent = Vue.component('actions-component', { props: { actions: { type: Array, diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 index aed65b33c04..6592c1b5f0f 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -5,7 +5,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { + gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', { props: { externalUrl: { type: String, diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index b26a40aa268..0e6bc3fdb2c 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -29,12 +29,12 @@ gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { components: { - 'commit-component': window.gl.CommitComponent, - 'actions-component': window.gl.environmentsList.ActionsComponent, - 'external-url-component': window.gl.environmentsList.ExternalUrlComponent, - 'stop-component': window.gl.environmentsList.StopComponent, - 'rollback-component': window.gl.environmentsList.RollbackComponent, - 'terminal-button-component': window.gl.environmentsList.TerminalButtonComponent, + 'commit-component': gl.CommitComponent, + 'actions-component': gl.environmentsList.ActionsComponent, + 'external-url-component': gl.environmentsList.ExternalUrlComponent, + 'stop-component': gl.environmentsList.StopComponent, + 'rollback-component': gl.environmentsList.RollbackComponent, + 'terminal-button-component': gl.environmentsList.TerminalButtonComponent, }, props: { @@ -183,7 +183,7 @@ * @returns {String} */ createdDate() { - return window.gl.environmentsList.timeagoInstance.format( + return gl.environmentsList.timeagoInstance.format( this.model.last_deployment.deployable.created_at, ); }, diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 index 6d4e8fad604..b52298b4a88 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js.es6 +++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6 @@ -5,7 +5,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { + gl.environmentsList.RollbackComponent = Vue.component('rollback-component', { props: { retryUrl: { type: String, diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 index 7292f924e5c..0a29f2f36e9 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js.es6 +++ b/app/assets/javascripts/environments/components/environment_stop.js.es6 @@ -5,7 +5,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.StopComponent = Vue.component('stop-component', { + gl.environmentsList.StopComponent = Vue.component('stop-component', { props: { stopUrl: { type: String, diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 index 25e6ac7f3c9..050184ba497 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 @@ -5,7 +5,7 @@ window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; - window.gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { + gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { props: { terminalPath: { type: String, diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 20eee7976ec..3b003f6f661 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -3,19 +3,20 @@ //= require ./components/environment //= require ./vue_resource_interceptor - $(() => { window.gl = window.gl || {}; - if (window.gl.EnvironmentsListApp) { - window.gl.EnvironmentsListApp.$destroy(true); + if (gl.EnvironmentsListApp) { + gl.EnvironmentsListApp.$destroy(true); } - const Store = window.gl.environmentsList.EnvironmentsStore; + const Store = gl.environmentsList.EnvironmentsStore; - window.gl.EnvironmentsListApp = new window.gl.environmentsList.EnvironmentsComponent({ + gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({ el: document.querySelector('#environments-list-view'), + propsData: { store: Store.create(), }, + }); }); diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 index 0204a903ab5..9b4090100da 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environments_store.js.es6 @@ -10,6 +10,8 @@ this.state.environments = []; this.state.stoppedCounter = 0; this.state.availableCounter = 0; + this.state.visibility = 'available'; + this.state.filteredEnvironments = []; return this; }, @@ -59,7 +61,7 @@ if (occurs.length) { acc[acc.indexOf(occurs[0])].children.push(environment); - acc[acc.indexOf(occurs[0])].children.sort(this.sortByName); + acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName); } else { acc.push({ name: environment.environment_type, @@ -73,13 +75,70 @@ } return acc; - }, []).sort(this.sortByName); + }, []).slice().sort(this.sortByName); this.state.environments = environmentsTree; + this.filterEnvironmentsByVisibility(this.state.environments); + return environmentsTree; }, + storeVisibility(visibility) { + this.state.visibility = visibility; + }, + /** + * Given the visibility prop provided by the url query parameter and which + * changes according to the active tab we need to filter which environments + * should be visible. + * + * The environments array is a recursive tree structure and we need to filter + * both root level environments and children environments. + * + * In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility` + * functions work together. + * The first one works as the filter that verifies if the given environment matches + * the given state. + * The second guarantees both root level and children elements are filtered as well. + * + * Given array of environments will return only + * the environments that match the state stored. + * + * @param {Array} array + * @return {Array} + */ + filterEnvironmentsByVisibility(arr) { + const filteredEnvironments = arr.map((item) => { + if (item.children) { + const filteredChildren = this.filterEnvironmentsByVisibility( + item.children, + ).filter(Boolean); + + if (filteredChildren.length) { + item.children = filteredChildren; + return item; + } + } + + return this.filterState(this.state.visibility, item); + }).filter(Boolean); + + this.state.filteredEnvironments = filteredEnvironments; + return filteredEnvironments; + }, + + /** + * Given the state and the environment, + * returns only if the environment state matches the one provided. + * + * @param {String} state + * @param {Object} environment + * @return {Object} + */ + filterState(state, environment) { + return environment.state === state && environment; + }, + /** * Toggles folder open property given the environment type. * diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6 index 717566a4715..cd401277689 100644 --- a/app/assets/javascripts/extensions/array.js.es6 +++ b/app/assets/javascripts/extensions/array.js.es6 @@ -1,11 +1,11 @@ -/* eslint-disable no-extend-native, func-names, space-before-function-paren, semi, space-infix-ops, max-len */ +/* eslint-disable no-extend-native, func-names, space-before-function-paren, space-infix-ops, max-len */ Array.prototype.first = function() { return this[0]; -} +}; Array.prototype.last = function() { return this[this.length-1]; -} +}; Array.prototype.find = Array.prototype.find || function(predicate, ...args) { if (!this) throw new TypeError('Array.prototype.find called on null or undefined'); diff --git a/app/assets/javascripts/extensions/custom_event.js.es6 b/app/assets/javascripts/extensions/custom_event.js.es6 new file mode 100644 index 00000000000..abedae4c1c7 --- /dev/null +++ b/app/assets/javascripts/extensions/custom_event.js.es6 @@ -0,0 +1,12 @@ +/* global CustomEvent */ +/* eslint-disable no-global-assign */ + +// Custom event support for IE +CustomEvent = function CustomEvent(event, parameters) { + const params = parameters || { bubbles: false, cancelable: false, detail: undefined }; + const evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; +}; + +CustomEvent.prototype = window.Event.prototype; diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6 index 3f12ad9ff9f..90ab79305a7 100644 --- a/app/assets/javascripts/extensions/element.js.es6 +++ b/app/assets/javascripts/extensions/element.js.es6 @@ -1,5 +1,5 @@ /* global Element */ -/* eslint-disable consistent-return, max-len, no-empty, no-plusplus, func-names */ +/* eslint-disable consistent-return, max-len, no-empty, func-names */ Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) { if (!selectedElement) return; @@ -14,7 +14,7 @@ Element.prototype.matches = Element.prototype.matches || Element.prototype.webkitMatchesSelector || function (s) { const matches = (this.document || this.ownerDocument).querySelectorAll(s); - let i = matches.length; - while (--i >= 0 && matches.item(i) !== this) {} + let i = matches.length - 1; + while (i >= 0 && matches.item(i) !== this) { i -= 1; } return i > -1; }; diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js index cdedc865d1b..d3b58b2707a 100644 --- a/app/assets/javascripts/extensions/jquery.js +++ b/app/assets/javascripts/extensions/jquery.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, object-shorthand, comma-dangle, max-len */ // Disable an element and add the 'disabled' Bootstrap class (function() { $.fn.extend({ @@ -13,5 +13,4 @@ return $(this).removeAttr('disabled').removeClass('disabled'); } }); - }).call(this); diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 785f2869970..895a872568d 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -1,8 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ /* global FilesCommentButton */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.FilesCommentButton = (function() { var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; @@ -132,7 +132,6 @@ }; return FilesCommentButton; - })(); $.fn.filesCommentButton = function() { @@ -145,5 +144,4 @@ } }); }; - }).call(this); diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 new file mode 100644 index 00000000000..7d297b8eee8 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js.es6 @@ -0,0 +1,69 @@ +/*= require filtered_search/filtered_search_dropdown */ + +/* global droplabFilter */ + +(() => { + class DropdownHint extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + droplabFilter: { + template: 'hint', + filterFunction: gl.DropdownUtils.filterHint.bind(null, input), + }, + }; + } + + itemClicked(e) { + const { selected } = e.detail; + + if (selected.tagName === 'LI') { + if (selected.hasAttribute('data-value')) { + this.dismissDropdown(); + } else if (selected.getAttribute('data-action') === 'submit') { + this.dismissDropdown(); + this.dispatchFormSubmitEvent(); + } else { + const token = selected.querySelector('.js-filter-hint').innerText.trim(); + const tag = selected.querySelector('.js-filter-tag').innerText.trim(); + + if (tag.length) { + gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', '')); + } + this.dismissDropdown(); + this.dispatchInputEvent(); + } + } + } + + renderContent() { + const dropdownData = [{ + icon: 'fa-pencil', + hint: 'author:', + tag: '<@author>', + }, { + icon: 'fa-user', + hint: 'assignee:', + tag: '<@assignee>', + }, { + icon: 'fa-clock-o', + hint: 'milestone:', + tag: '<%milestone>', + }, { + icon: 'fa-tag', + hint: 'label:', + tag: '<~label>', + }]; + + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); + this.droplab.setData(this.hookId, dropdownData); + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownHint = DropdownHint; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 new file mode 100644 index 00000000000..13cbec1be4a --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js.es6 @@ -0,0 +1,44 @@ +/*= require filtered_search/filtered_search_dropdown */ + +/* global droplabAjax */ +/* global droplabFilter */ + +(() => { + class DropdownNonUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter, endpoint, symbol) { + super(droplab, dropdown, input, filter); + this.symbol = symbol; + this.config = { + droplabAjax: { + endpoint, + method: 'setData', + loadingTemplate: this.loadingTemplate, + }, + droplabFilter: { + filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), + }, + }; + } + + itemClicked(e) { + super.itemClicked(e, (selected) => { + const title = selected.querySelector('.js-data-value').innerText.trim(); + return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`; + }); + } + + renderContent(forceShowList = false) { + this.droplab + .changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); + super.renderContent(forceShowList); + } + + init() { + this.droplab + .addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownNonUser = DropdownNonUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 new file mode 100644 index 00000000000..7bf199d9274 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -0,0 +1,53 @@ +/*= require filtered_search/filtered_search_dropdown */ + +/* global droplabAjaxFilter */ + +(() => { + class DropdownUser extends gl.FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + super(droplab, dropdown, input, filter); + this.config = { + droplabAjaxFilter: { + endpoint: '/autocomplete/users.json', + searchKey: 'search', + params: { + per_page: 20, + active: true, + project_id: this.getProjectId(), + current_user: true, + }, + searchValueFunction: this.getSearchInput.bind(this), + loadingTemplate: this.loadingTemplate, + }, + }; + } + + itemClicked(e) { + super.itemClicked(e, + selected => selected.querySelector('.dropdown-light-content').innerText.trim()); + } + + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); + super.renderContent(forceShowList); + } + + getProjectId() { + return this.input.getAttribute('data-project-id'); + } + + getSearchInput() { + const query = gl.DropdownUtils.getSearchInput(this.input); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + + return lastToken.value || ''; + } + + init() { + this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); + } + } + + window.gl = window.gl || {}; + gl.DropdownUser = DropdownUser; +})(); diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 new file mode 100644 index 00000000000..de3fa116717 --- /dev/null +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 @@ -0,0 +1,126 @@ +(() => { + class DropdownUtils { + static getEscapedText(text) { + let escapedText = text; + const hasSpace = text.indexOf(' ') !== -1; + const hasDoubleQuote = text.indexOf('"') !== -1; + + // Encapsulate value with quotes if it has spaces + // Known side effect: values's with both single and double quotes + // won't escape properly + if (hasSpace) { + if (hasDoubleQuote) { + escapedText = `'${text}'`; + } else { + // Encapsulate singleQuotes or if it hasSpace + escapedText = `"${text}"`; + } + } + + return escapedText; + } + + static filterWithSymbol(filterSymbol, input, item) { + const updatedItem = item; + const query = gl.DropdownUtils.getSearchInput(input); + const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query); + + if (lastToken !== searchToken) { + const title = updatedItem.title.toLowerCase(); + let value = lastToken.value.toLowerCase(); + + // Removes the first character if it is a quotation so that we can search + // with multiple words + if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) { + value = value.slice(1); + } + + // Eg. filterSymbol = ~ for labels + const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1; + const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1; + + updatedItem.droplab_hidden = !match && !matchWithoutSymbol; + } else { + updatedItem.droplab_hidden = false; + } + + return updatedItem; + } + + static filterHint(input, item) { + const updatedItem = item; + const query = gl.DropdownUtils.getSearchInput(input); + let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + lastToken = lastToken.key || lastToken || ''; + + if (!lastToken || query.split('').last() === ' ') { + updatedItem.droplab_hidden = false; + } else if (lastToken) { + const split = lastToken.split(':'); + const tokenName = split[0].split(' ').last(); + + const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1; + updatedItem.droplab_hidden = tokenName ? match : false; + } + + return updatedItem; + } + + static setDataValueIfSelected(filter, selected) { + const dataValue = selected.getAttribute('data-value'); + + if (dataValue) { + gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue); + } + + // Return boolean based on whether it was set + return dataValue !== null; + } + + static getSearchInput(filteredSearchInput) { + const inputValue = filteredSearchInput.value; + const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput); + + return inputValue.slice(0, right); + } + + static getInputSelectionPosition(input) { + const selectionStart = input.selectionStart; + let inputValue = input.value; + // Replace all spaces inside quote marks with underscores + // (will continue to match entire string until an end quote is found if any) + // This helps with matching the beginning & end of a token:key + inputValue = inputValue.replace(/(('[^']*'{0,1})|("[^"]*"{0,1})|:\s+)/g, str => str.replace(/\s/g, '_')); + + // Get the right position for the word selected + // Regex matches first space + let right = inputValue.slice(selectionStart).search(/\s/); + + if (right >= 0) { + right += selectionStart; + } else if (right < 0) { + right = inputValue.length; + } + + // Get the left position for the word selected + // Regex matches last non-whitespace character + let left = inputValue.slice(0, right).search(/\S+$/); + + if (selectionStart === 0) { + left = 0; + } else if (selectionStart === inputValue.length && left < 0) { + left = inputValue.length; + } else if (left < 0) { + left = selectionStart; + } + + return { + left, + right, + }; + } + } + + window.gl = window.gl || {}; + gl.DropdownUtils = DropdownUtils; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js new file mode 100644 index 00000000000..d188718c5f3 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -0,0 +1,7 @@ + // This is a manifest file that'll be compiled into including all the files listed below. + // Add new JavaScript code in separate files in this directory and they'll automatically + // be included in the compiled file accessible from http://example.com/assets/application.js + // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the + // the compiled file. + // + /*= require_tree . */ diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 new file mode 100644 index 00000000000..859d6515531 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js.es6 @@ -0,0 +1,112 @@ +(() => { + const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger'; + + class FilteredSearchDropdown { + constructor(droplab, dropdown, input, filter) { + this.droplab = droplab; + this.hookId = input.getAttribute('data-id'); + this.input = input; + this.filter = filter; + this.dropdown = dropdown; + this.loadingTemplate = `<div class="filter-dropdown-loading"> + <i class="fa fa-spinner fa-spin"></i> + </div>`; + this.bindEvents(); + } + + bindEvents() { + this.itemClickedWrapper = this.itemClicked.bind(this); + this.dropdown.addEventListener('click.dl', this.itemClickedWrapper); + } + + unbindEvents() { + this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper); + } + + getCurrentHook() { + return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null; + } + + itemClicked(e, getValueFunction) { + const { selected } = e.detail; + + if (selected.tagName === 'LI' && selected.innerHTML) { + const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected); + + if (!dataValueSet) { + const value = getValueFunction(selected); + gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value); + } + + this.dismissDropdown(); + this.dispatchInputEvent(); + } + } + + setAsDropdown() { + this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`); + } + + setOffset(offset = 0) { + this.dropdown.style.left = `${offset}px`; + } + + renderContent(forceShowList = false) { + if (forceShowList && this.getCurrentHook().list.hidden) { + this.getCurrentHook().list.show(); + } + } + + render(forceRenderContent = false, forceShowList = false) { + this.setAsDropdown(); + + const currentHook = this.getCurrentHook(); + const firstTimeInitialized = currentHook === null; + + if (firstTimeInitialized || forceRenderContent) { + this.renderContent(forceShowList); + } else if (currentHook.list.list.id !== this.dropdown.id) { + this.renderContent(forceShowList); + } + } + + dismissDropdown() { + // Focusing on the input will dismiss dropdown + // (default droplab functionality) + this.input.focus(); + } + + dispatchInputEvent() { + // Propogate input change to FilteredSearchDropdownManager + // so that it can determine which dropdowns to open + this.input.dispatchEvent(new CustomEvent('input', { + bubbles: true, + cancelable: true, + })); + } + + dispatchFormSubmitEvent() { + // dispatchEvent() is necessary as form.submit() does not + // trigger event handlers + this.input.form.dispatchEvent(new Event('submit')); + } + + hideDropdown() { + this.getCurrentHook().list.hide(); + } + + resetFilters() { + const hook = this.getCurrentHook(); + const data = hook.list.data; + const results = data.map((o) => { + const updated = o; + updated.droplab_hidden = false; + return updated; + }); + hook.list.render(results); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchDropdown = FilteredSearchDropdown; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 new file mode 100644 index 00000000000..00e1c28692f --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -0,0 +1,207 @@ +/* global DropLab */ + +(() => { + class FilteredSearchDropdownManager { + constructor() { + this.tokenizer = gl.FilteredSearchTokenizer; + this.filteredSearchInput = document.querySelector('.filtered-search'); + + this.setupMapping(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); + } + + cleanup() { + if (this.droplab) { + this.droplab.destroy(); + this.droplab = null; + } + + this.setupMapping(); + + document.removeEventListener('page:fetch', this.cleanupWrapper); + } + + setupMapping() { + this.mapping = { + author: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-author'), + }, + assignee: { + reference: null, + gl: 'DropdownUser', + element: document.querySelector('#js-dropdown-assignee'), + }, + milestone: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['milestones.json', '%'], + element: document.querySelector('#js-dropdown-milestone'), + }, + label: { + reference: null, + gl: 'DropdownNonUser', + extraArguments: ['labels.json', '~'], + element: document.querySelector('#js-dropdown-label'), + }, + hint: { + reference: null, + gl: 'DropdownHint', + element: document.querySelector('#js-dropdown-hint'), + }, + }; + } + + static addWordToInput(tokenName, tokenValue = '') { + const input = document.querySelector('.filtered-search'); + const inputValue = input.value; + const word = `${tokenName}:${tokenValue}`; + + // Get the string to replace + let newCaretPosition = input.selectionStart; + const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input); + + input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`; + + // If we have added a tokenValue at the end of the input, + // add a space and set selection to the end + if (right >= inputValue.length && tokenValue !== '') { + input.value += ' '; + newCaretPosition = input.value.length; + } + + gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input); + } + + static updateInputCaretPosition(selectionStart, input) { + // Reset the position + // Sometimes can end up at end of input + input.setSelectionRange(selectionStart, selectionStart); + + const { right } = gl.DropdownUtils.getInputSelectionPosition(input); + + input.setSelectionRange(right, right); + } + + updateCurrentDropdownOffset() { + this.updateDropdownOffset(this.currentDropdown); + } + + updateDropdownOffset(key) { + if (!this.font) { + this.font = window.getComputedStyle(this.filteredSearchInput).font; + } + + const input = this.filteredSearchInput; + const inputText = input.value.slice(0, input.selectionStart); + const filterIconPadding = 27; + let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding; + + const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 : + this.mapping[key].element.clientWidth; + const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth; + + if (offsetMaxWidth < offset) { + offset = offsetMaxWidth; + } + + this.mapping[key].reference.setOffset(offset); + } + + load(key, firstLoad = false) { + const mappingKey = this.mapping[key]; + const glClass = mappingKey.gl; + const element = mappingKey.element; + let forceShowList = false; + + if (!mappingKey.reference) { + const dl = this.droplab; + const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; + const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); + + // Passing glArguments to `new gl[glClass](<arguments>)` + mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))(); + } + + if (firstLoad) { + mappingKey.reference.init(); + } + + if (this.currentDropdown === 'hint') { + // Force the dropdown to show if it was clicked from the hint dropdown + forceShowList = true; + } + + this.updateDropdownOffset(key); + mappingKey.reference.render(firstLoad, forceShowList); + + this.currentDropdown = key; + } + + loadDropdown(dropdownName = '') { + let firstLoad = false; + + if (!this.droplab) { + firstLoad = true; + this.droplab = new DropLab(); + } + + const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase()); + const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key + && this.mapping[match.key]; + const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint'; + + if (shouldOpenFilterDropdown || shouldOpenHintDropdown) { + const key = match && match.key ? match.key : 'hint'; + this.load(key, firstLoad); + } + } + + setDropdown() { + const { lastToken, searchToken } = this.tokenizer + .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput)); + + if (this.currentDropdown) { + this.updateCurrentDropdownOffset(); + } + + if (lastToken === searchToken && lastToken !== null) { + // Token is not fully initialized yet because it has no value + // Eg. token = 'label:' + + const split = lastToken.split(':'); + const dropdownName = split[0].split(' ').last(); + this.loadDropdown(split.length > 1 ? dropdownName : ''); + } else if (lastToken) { + // Token has been initialized into an object because it has a value + this.loadDropdown(lastToken.key); + } else { + this.loadDropdown('hint'); + } + } + + resetDropdowns() { + // Force current dropdown to hide + this.mapping[this.currentDropdown].reference.hideDropdown(); + + // Re-Load dropdown + this.setDropdown(); + + // Reset filters for current dropdown + this.mapping[this.currentDropdown].reference.resetFilters(); + + // Reposition dropdown so that it is aligned with cursor + this.updateDropdownOffset(this.currentDropdown); + } + + destroyDroplab() { + this.droplab.destroy(); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 new file mode 100644 index 00000000000..029564ffc61 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -0,0 +1,230 @@ +/* global Turbolinks */ + +(() => { + class FilteredSearchManager { + constructor() { + this.filteredSearchInput = document.querySelector('.filtered-search'); + this.clearSearchButton = document.querySelector('.clear-search'); + + if (this.filteredSearchInput) { + this.tokenizer = gl.FilteredSearchTokenizer; + this.dropdownManager = new gl.FilteredSearchDropdownManager(); + + this.bindEvents(); + this.loadSearchParamsFromURL(); + this.dropdownManager.setDropdown(); + + this.cleanupWrapper = this.cleanup.bind(this); + document.addEventListener('page:fetch', this.cleanupWrapper); + } + } + + cleanup() { + this.unbindEvents(); + document.removeEventListener('page:fetch', this.cleanupWrapper); + } + + bindEvents() { + this.handleFormSubmit = this.handleFormSubmit.bind(this); + this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager); + this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this); + this.checkForEnterWrapper = this.checkForEnter.bind(this); + this.clearSearchWrapper = this.clearSearch.bind(this); + this.checkForBackspaceWrapper = this.checkForBackspace.bind(this); + this.tokenChange = this.tokenChange.bind(this); + + this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit); + this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); + this.filteredSearchInput.addEventListener('click', this.tokenChange); + this.filteredSearchInput.addEventListener('keyup', this.tokenChange); + this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); + } + + unbindEvents() { + this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit); + this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper); + this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper); + this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper); + this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); + this.filteredSearchInput.removeEventListener('click', this.tokenChange); + this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); + this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); + } + + checkForBackspace(e) { + // 8 = Backspace Key + // 46 = Delete Key + if (e.keyCode === 8 || e.keyCode === 46) { + // Reposition dropdown so that it is aligned with cursor + this.dropdownManager.updateCurrentDropdownOffset(); + } + } + + checkForEnter(e) { + if (e.keyCode === 38 || e.keyCode === 40) { + const selectionStart = this.filteredSearchInput.selectionStart; + + e.preventDefault(); + this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart); + } + + if (e.keyCode === 13) { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + const dropdownEl = dropdown.element; + const activeElements = dropdownEl.querySelectorAll('.dropdown-active'); + + e.preventDefault(); + + if (!activeElements.length) { + // Prevent droplab from opening dropdown + this.dropdownManager.destroyDroplab(); + + this.search(); + } + } + } + + toggleClearSearchButton(e) { + if (e.target.value) { + this.clearSearchButton.classList.remove('hidden'); + } else { + this.clearSearchButton.classList.add('hidden'); + } + } + + clearSearch(e) { + e.preventDefault(); + + this.filteredSearchInput.value = ''; + this.clearSearchButton.classList.add('hidden'); + + this.dropdownManager.resetDropdowns(); + } + + handleFormSubmit(e) { + e.preventDefault(); + this.search(); + } + + loadSearchParamsFromURL() { + const params = gl.utils.getUrlParamsArray(); + const usernameParams = this.getUsernameParams(); + const inputValues = []; + + params.forEach((p) => { + const split = p.split('='); + const keyParam = decodeURIComponent(split[0]); + const value = split[1]; + + // Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p); + + if (condition) { + inputValues.push(`${condition.tokenKey}:${condition.value}`); + } else { + // Sanitize value since URL converts spaces into + + // Replace before decode so that we know what was originally + versus the encoded + + const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value; + const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam); + + if (match) { + const indexOf = keyParam.indexOf('_'); + const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam; + const symbol = match.symbol; + let quotationsToUse = ''; + + if (sanitizedValue.indexOf(' ') !== -1) { + // Prefer ", but use ' if required + quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\''; + } + + inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`); + } else if (!match && keyParam === 'assignee_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + inputValues.push(`assignee:@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'author_id') { + const id = parseInt(value, 10); + if (usernameParams[id]) { + inputValues.push(`author:@${usernameParams[id]}`); + } + } else if (!match && keyParam === 'search') { + inputValues.push(sanitizedValue); + } + } + }); + + // Trim the last space value + this.filteredSearchInput.value = inputValues.join(' '); + + if (inputValues.length > 0) { + this.clearSearchButton.classList.remove('hidden'); + } + } + + search() { + const paths = []; + const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value); + const currentState = gl.utils.getParameterByName('state') || 'opened'; + paths.push(`state=${currentState}`); + + tokens.forEach((token) => { + const condition = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(token.key, token.value.toLowerCase()); + const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key); + const keyParam = param ? `${token.key}_${param}` : token.key; + let tokenPath = ''; + + if (condition) { + tokenPath = condition.url; + } else { + let tokenValue = token.value; + + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || + (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { + tokenValue = tokenValue.slice(1, tokenValue.length - 1); + } + + tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`; + } + + paths.push(tokenPath); + }); + + if (searchToken) { + const sanitized = searchToken.split(' ').map(t => encodeURIComponent(t)).join('+'); + paths.push(`search=${sanitized}`); + } + + Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); + } + + getUsernameParams() { + const usernamesById = {}; + try { + const attribute = this.filteredSearchInput.getAttribute('data-username-params'); + JSON.parse(attribute).forEach((user) => { + usernamesById[user.id] = user.username; + }); + } catch (e) { + // do nothing + } + return usernamesById; + } + + tokenChange() { + const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; + const currentDropdownRef = dropdown.reference; + + this.setDropdownWrapper(); + currentDropdownRef.dispatchInputEvent(); + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchManager = FilteredSearchManager; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 new file mode 100644 index 00000000000..e6b53cd4b55 --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js.es6 @@ -0,0 +1,96 @@ +(() => { + const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + }, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + }, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + }]; + + const alternativeTokenKeys = [{ + key: 'label', + type: 'string', + param: 'name', + symbol: '~', + }]; + + const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); + + const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', + }, { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', + }, { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', + }, { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', + }]; + + class FilteredSearchTokenKeys { + static get() { + return tokenKeys; + } + + static getAlternatives() { + return alternativeTokenKeys; + } + + static getConditions() { + return conditions; + } + + static searchByKey(key) { + return tokenKeys.find(tokenKey => tokenKey.key === key) || null; + } + + static searchBySymbol(symbol) { + return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + } + + static searchByKeyParam(keyParam) { + return tokenKeysWithAlternative.find((tokenKey) => { + let tokenKeyParam = tokenKey.key; + + if (tokenKey.param) { + tokenKeyParam += `_${tokenKey.param}`; + } + + return keyParam === tokenKeyParam; + }) || null; + } + + static searchByConditionUrl(url) { + return conditions.find(condition => condition.url === url) || null; + } + + static searchByConditionKeyValue(key, value) { + return conditions + .find(condition => condition.tokenKey === key && condition.value === value) || null; + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys; +})(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 new file mode 100644 index 00000000000..cf53845a48b --- /dev/null +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js.es6 @@ -0,0 +1,45 @@ +(() => { + class FilteredSearchTokenizer { + static processTokens(input) { + // Regex extracts `(token):(symbol)(value)` + // Values that start with a double quote must end in a double quote (same for single) + const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g; + const tokens = []; + let lastToken = null; + const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => { + let tokenValue = v1 || v2 || v3; + let tokenSymbol = symbol; + + if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') { + tokenSymbol = tokenValue; + tokenValue = ''; + } + + tokens.push({ + key, + value: tokenValue || '', + symbol: tokenSymbol || '', + }); + return ''; + }).replace(/\s{2,}/g, ' ').trim() || ''; + + if (tokens.length > 0) { + const last = tokens[tokens.length - 1]; + const lastString = `${last.key}:${last.symbol}${last.value}`; + lastToken = input.lastIndexOf(lastString) === + input.length - lastString.length ? last : searchToken; + } else { + lastToken = searchToken; + } + + return { + tokens, + lastToken, + searchToken, + }; + } + } + + window.gl = window.gl || {}; + gl.FilteredSearchTokenizer = FilteredSearchTokenizer; +})(); diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js index 804d7d9c4ab..249fe23d4cb 100644 --- a/app/assets/javascripts/flash.js +++ b/app/assets/javascripts/flash.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-param-reassign, quotes, quote-props, prefer-template, comma-dangle, max-len */ (function() { this.Flash = (function() { var hideFlash; @@ -38,7 +38,5 @@ } return Flash; - })(); - }).call(this); diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 87c579ac757..3f23095dad9 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ // Creates the variables for setting up GFM auto-completion (function() { @@ -48,18 +48,18 @@ }, DefaultOptions: { sorter: function(query, items, searchKey) { + this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; if (gl.GfmAutoComplete.isLoading(items)) { + this.setting.highlightFirst = false; return items; } return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); }, filter: function(query, data, searchKey) { if (gl.GfmAutoComplete.isLoading(data)) { - gl.GfmAutoComplete.togglePreventSelection.call(this, true); gl.GfmAutoComplete.fetchData(this.$inputor, this.at); return data; } else { - gl.GfmAutoComplete.togglePreventSelection.call(this, false); return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); } }, @@ -154,7 +154,7 @@ return { username: m.username, - avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, + avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, title: sanitize(title), search: sanitize(m.username + " " + m.name) }; @@ -257,9 +257,9 @@ insertTpl: '${atwho-at}${title}', callbacks: { matcher: this.DefaultOptions.matcher, - sorter: this.DefaultOptions.sorter, beforeInsert: this.DefaultOptions.beforeInsert, filter: this.DefaultOptions.filter, + sorter: this.DefaultOptions.sorter, beforeSave: function(merges) { if (gl.GfmAutoComplete.isLoading(merges)) return merges; var sanitizeLabelTitle; @@ -335,7 +335,7 @@ }); }, matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { - var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi + var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; var match = regexp.exec(subtext); if (match) { return match[1]; @@ -367,14 +367,14 @@ return $input.trigger('keyup'); }, isLoading(data) { - if (!data || !data.length) return false; - if (Array.isArray(data)) data = data[0]; - return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0]; - }, - togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) { - this.setting.tabSelectsMatch = !isPrevented; - this.setting.spaceSelectsMatch = !isPrevented; - }, - }; + var dataToInspect = data; + if (data && data.length > 0) { + dataToInspect = data[0]; + } + var loadingState = this.defaultLoadingData[0]; + return dataToInspect && + (dataToInspect === loadingState || dataToInspect.name === loadingState); + } + }; }).call(this); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index bb516b3d2df..cc1c0877cdf 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,11 +1,11 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* global fuzzaldrinPlus */ /* global Turbolinks */ (function() { var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, - bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, - indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; GitLabDropdownFilter = (function() { var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS; @@ -37,7 +37,7 @@ .on('keydown', function (e) { var keyCode = e.which; if (keyCode === 13 && !options.elIsInput) { - e.preventDefault() + e.preventDefault(); } }) .on('input', function() { @@ -133,7 +133,6 @@ }; return GitLabDropdownFilter; - })(); GitLabDropdownRemote = (function() { @@ -186,7 +185,6 @@ }; return GitLabDropdownRemote; - })(); GitLabDropdown = (function() { @@ -206,7 +204,7 @@ SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)"; - CURSOR_SELECT_SCROLL_PADDING = 5 + CURSOR_SELECT_SCROLL_PADDING = 5; FILTER_INPUT = '.dropdown-input .dropdown-input-field'; @@ -223,7 +221,7 @@ this.dropdown = selector != null ? $(selector) : $(this.el).parent(); // Set Defaults this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT); - this.highlight = !!this.options.highlight + this.highlight = !!this.options.highlight; this.filterInputBlur = this.options.filterInputBlur != null ? this.options.filterInputBlur : true; @@ -476,7 +474,7 @@ this.removeArrayKeyEvent(); $input = this.dropdown.find(".dropdown-input-field"); if (this.options.filterable) { - $input.blur().val(""); + $input.blur(); } if (this.dropdown.find(".dropdown-toggle-page").length) { $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS); @@ -494,7 +492,7 @@ } else { var ul = document.createElement('ul'); - for (var i = 0; i < html.length; i++) { + for (var i = 0; i < html.length; i += 1) { var el = html[i]; if (el instanceof jQuery) { @@ -550,7 +548,7 @@ value = this.options.id ? this.options.id(data) : data.id; fieldName = this.options.fieldName; - if (value) { value = value.toString().replace(/'/g, '\\\'') }; + if (value) { value = value.toString().replace(/'/g, '\\\''); } field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']"); if (field.length) { @@ -641,7 +639,7 @@ : selectedObject.id; if (isInput) { field = $(this.el); - } else if(value) { + } else if (value) { field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value.toString().replace(/'/g, '\\\'') + "']"); } @@ -653,18 +651,14 @@ isMarking = false; el.removeClass(ACTIVE_CLASS); if (field && field.length) { - if (isInput) { - field.val(''); - } else { - field.remove(); - } + this.clearField(field, isInput); } } else if (el.hasClass(INDETERMINATE_CLASS)) { isMarking = true; el.addClass(ACTIVE_CLASS); el.removeClass(INDETERMINATE_CLASS); if (field && field.length && value == null) { - field.remove(); + this.clearField(field, isInput); } if ((!field || !field.length) && fieldName) { this.addInput(fieldName, value, selectedObject); @@ -678,7 +672,7 @@ } } if (field && field.length && value == null) { - field.remove(); + this.clearField(field, isInput); } // Toggle active class for the tick mark el.addClass(ACTIVE_CLASS); @@ -695,8 +689,8 @@ }; GitLabDropdown.prototype.focusTextInput = function() { - if (this.options.filterable) { this.filterInput.focus() } - } + if (this.options.filterable) { this.filterInput.focus(); } + }; GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) { var $input; @@ -802,7 +796,7 @@ listItemBottom = listItemTop + listItemHeight; if (!index) { // Scroll the dropdown content to the top - $dropdownContent.scrollTop(0) + $dropdownContent.scrollTop(0); } else if (index === ($listItems.length - 1)) { // Scroll the dropdown content to the bottom $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight')); @@ -828,8 +822,11 @@ return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance)); }; - return GitLabDropdown; + GitLabDropdown.prototype.clearField = function(field, isInput) { + return isInput ? field.val('') : field.remove(); + }; + return GitLabDropdown; })(); $.fn.glDropdown = function(opts) { @@ -839,5 +836,4 @@ } }); }; - }).call(this); diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 index 63f9cafa8d0..16be930a2f4 100644 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign, padded-blocks */ +/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ //= require gl_field_error @@ -45,5 +45,4 @@ } global.GlFieldErrors = GlFieldErrors; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 7dc2d13e5d8..08b2494f3df 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */ /* global GitLab */ /* global DropzoneInput */ /* global autosize */ @@ -35,8 +35,8 @@ autosize(this.textarea); // form and textarea event listeners this.addEventListeners(); - gl.text.init(this.form); } + gl.text.init(this.form); // hide discard button this.form.find('.js-note-discard').hide(); return this.form.show(); @@ -58,7 +58,5 @@ }; return GLForm; - })(); - }).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js index 3273bf3a263..2e6da5750de 100644 --- a/app/assets/javascripts/graphs/stat_graph.js +++ b/app/assets/javascripts/graphs/stat_graph.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-return-assign, max-len */ (function() { this.StatGraph = (function() { function StatGraph() {} @@ -14,7 +14,5 @@ }; return StatGraph; - })(); - }).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index 2d08a7c6ac3..73715286c4a 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign */ /* global ContributorsGraph */ /* global ContributorsAuthorGraph */ /* global ContributorsMasterGraph */ @@ -112,7 +112,5 @@ }; return ContributorsStatGraph; - })(); - }).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 9c5e9381e52..cacfc177fc8 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,11 +1,11 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, newline-per-chained-call, no-else-return */ +/* eslint-disable func-names, space-before-function-paren, one-var, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return */ /* global d3 */ /* global ContributorsGraph */ /*= require d3 */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, 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; @@ -91,7 +91,6 @@ }; return ContributorsGraph; - })(); this.ContributorsMasterGraph = (function(superClass) { @@ -196,7 +195,6 @@ }; return ContributorsMasterGraph; - })(ContributorsGraph); this.ContributorsAuthorGraph = (function(superClass) { @@ -274,7 +272,5 @@ }; return ContributorsAuthorGraph; - })(ContributorsGraph); - }).call(this); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js index 1982f4af939..29c3163328f 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, no-plusplus, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, object-shorthand, no-var, one-var, camelcase, one-var-declaration-per-line, comma-dangle, no-param-reassign, no-return-assign, quotes, prefer-arrow-callback, wrap-iife, consistent-return, no-unused-vars, max-len, no-cond-assign, no-else-return, max-len */ (function() { window.ContributorsStatGraphUtil = { parse_log: function(log) { @@ -6,7 +6,7 @@ total = {}; by_author = {}; by_email = {}; - for (i = 0, len = log.length; i < len; i++) { + for (i = 0, len = log.length; i < len; i += 1) { entry = log[i]; if (total[entry.date] == null) { this.add_date(entry.date, total); @@ -135,5 +135,4 @@ } } }; - }).call(this); diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js index 17a76168a79..10dfd05fe3c 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -1,13 +1,13 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ (function() { this.GroupAvatar = (function() { function GroupAvatar() { - $('.js-choose-group-avatar-button').bind("click", function() { + $('.js-choose-group-avatar-button').on("click", function() { var form; form = $(this).closest("form"); return form.find(".js-group-avatar-input").click(); }); - $('.js-group-avatar-input').bind("change", function() { + $('.js-group-avatar-input').on("change", function() { var filename, form; form = $(this).closest("form"); filename = $(this).val().replace(/^.*[\\\/]/, ''); @@ -16,7 +16,5 @@ } return GroupAvatar; - })(); - }).call(this); diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6 index 8e10e424412..15e695e81cf 100644 --- a/app/assets/javascripts/group_label_subscription.js.es6 +++ b/app/assets/javascripts/group_label_subscription.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, max-len */ (function(global) { class GroupLabelSubscription { @@ -50,5 +50,4 @@ } global.GroupLabelSubscription = GroupLabelSubscription; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index 99700e7562a..a50bc4a9057 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, max-len */ /* global Api */ (function() { @@ -67,7 +67,5 @@ }; return GroupsSelect; - })(); - }).call(this); diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index c7cbf9ca44b..fa85f9a6c86 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,10 +1,8 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, padded-blocks, prefer-arrow-callback, no-var, max-len */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, no-var, max-len */ (function() { - $(document).on('todo:toggle', function(e, count) { var $todoPendingCount = $('.todos-pending-count'); $todoPendingCount.text(gl.text.addDelimiter(count)); $todoPendingCount.toggleClass('hidden', count === 0); }); - })(); diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index fa795be07ed..9390136d3d8 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, vars-on-top, no-new, max-len */ (function() { window.ImporterStatus = (function() { @@ -68,7 +68,6 @@ }; return ImporterStatus; - })(); $(function() { diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 9c3c96c20ed..f63d700fd65 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ +/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ /* global Issuable */ /* global Turbolinks */ @@ -34,7 +34,6 @@ e.preventDefault(); debouncedExecSearch(e); }); - }, initSearchState: function($searchInput) { const currentSearchVal = $searchInput.val(); @@ -152,7 +151,7 @@ this.issuableBulkActions.setOriginalDropdownData(); if ($checkedIssues.length > 0) { - let ids = $.map($checkedIssues, function(value) { + const ids = $.map($checkedIssues, function(value) { return $(value).data('id'); }); $updateIssuesIds.val(ids); @@ -187,5 +186,4 @@ }); } }; - })(window); diff --git a/app/assets/javascripts/issuable/issuable_bundle.js.es6 b/app/assets/javascripts/issuable/issuable_bundle.js.es6 new file mode 100644 index 00000000000..7d0465aa8b4 --- /dev/null +++ b/app/assets/javascripts/issuable/issuable_bundle.js.es6 @@ -0,0 +1 @@ +//= require ./time_tracking/time_tracking_bundle diff --git a/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 new file mode 100644 index 00000000000..72433df2818 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/collapsed_state.js.es6 @@ -0,0 +1,42 @@ +/* global Vue */ +//= require lib/utils/pretty_time + +(() => { + Vue.component('time-tracking-collapsed-state', { + name: 'time-tracking-collapsed-state', + props: [ + 'showComparisonState', + 'showSpentOnlyState', + 'showEstimateOnlyState', + 'showNoTimeTrackingState', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + 'stopwatchSvg', + ], + methods: { + abbreviateTime(timeStr) { + return gl.utils.prettyTime.abbreviateTime(timeStr); + }, + }, + template: ` + <div class='sidebar-collapsed-icon'> + <div v-html='stopwatchSvg'></div> + <div class='time-tracking-collapsed-summary'> + <div class='compare' v-if='showComparisonState'> + <span>{{ abbreviateTime(timeSpentHumanReadable) }} / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> + </div> + <div class='estimate-only' v-if='showEstimateOnlyState'> + <span class='bold'>-- / {{ abbreviateTime(timeEstimateHumanReadable) }}</span> + </div> + <div class='spend-only' v-if='showSpentOnlyState'> + <span class='bold'>{{ abbreviateTime(timeSpentHumanReadable) }} / --</span> + </div> + <div class='no-tracking' v-if='showNoTimeTrackingState'> + <span class='no-value'>None</span> + </div> + </div> + </div> + `, + }); +})(); + diff --git a/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 new file mode 100644 index 00000000000..6abbd5dd167 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/comparison_pane.js.es6 @@ -0,0 +1,69 @@ +/* global Vue */ +//= require lib/utils/pretty_time + +(() => { + const prettyTime = gl.utils.prettyTime; + + Vue.component('time-tracking-comparison-pane', { + name: 'time-tracking-comparison-pane', + props: [ + 'timeSpent', + 'timeEstimate', + 'timeSpentHumanReadable', + 'timeEstimateHumanReadable', + ], + computed: { + parsedRemaining() { + const diffSeconds = this.timeEstimate - this.timeSpent; + return prettyTime.parseSeconds(diffSeconds); + }, + timeRemainingHumanReadable() { + return prettyTime.stringifyTime(this.parsedRemaining); + }, + timeRemainingTooltip() { + const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:'; + return `${prefix} ${this.timeRemainingHumanReadable}`; + }, + /* Diff values for comparison meter */ + timeRemainingMinutes() { + return this.timeEstimate - this.timeSpent; + }, + timeRemainingPercent() { + return `${Math.floor((this.timeSpent / this.timeEstimate) * 100)}%`; + }, + timeRemainingStatusClass() { + return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate'; + }, + /* Parsed time values */ + parsedEstimate() { + return prettyTime.parseSeconds(this.timeEstimate); + }, + parsedSpent() { + return prettyTime.parseSeconds(this.timeSpent); + }, + }, + template: ` + <div class='time-tracking-comparison-pane'> + <div class='compare-meter' data-toggle='tooltip' data-placement='top' role='timeRemainingDisplay' + :aria-valuenow='timeRemainingTooltip' + :title='timeRemainingTooltip' + :data-original-title='timeRemainingTooltip' + :class='timeRemainingStatusClass'> + <div class='meter-container' role='timeSpentPercent' :aria-valuenow='timeRemainingPercent'> + <div :style='{ width: timeRemainingPercent }' class='meter-fill'></div> + </div> + <div class='compare-display-container'> + <div class='compare-display pull-left'> + <span class='compare-label'>Spent</span> + <span class='compare-value spent'>{{ timeSpentHumanReadable }}</span> + </div> + <div class='compare-display estimated pull-right'> + <span class='compare-label'>Est</span> + <span class='compare-value'>{{ timeEstimateHumanReadable }}</span> + </div> + </div> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 new file mode 100644 index 00000000000..309e9f2f9ef --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/estimate_only_pane.js.es6 @@ -0,0 +1,13 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-estimate-only-pane', { + name: 'time-tracking-estimate-only-pane', + props: ['timeEstimateHumanReadable'], + template: ` + <div class='time-tracking-estimate-only-pane'> + <span class='bold'>Estimated:</span> + {{ timeEstimateHumanReadable }} + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 new file mode 100644 index 00000000000..d7ced6d7151 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/help_state.js.es6 @@ -0,0 +1,24 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-help-state', { + name: 'time-tracking-help-state', + props: ['docsUrl'], + template: ` + <div class='time-tracking-help-state'> + <div class='time-tracking-info'> + <h4>Track time with slash commands</h4> + <p>Slash commands can be used in the issues description and comment boxes.</p> + <p> + <code>/estimate</code> + will update the estimated time with the latest command. + </p> + <p> + <code>/spend</code> + will update the sum of the time spent. + </p> + <a class='btn btn-default learn-more-button' :href='docsUrl'>Learn more</a> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 new file mode 100644 index 00000000000..1d2ca643b5b --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/no_tracking_pane.js.es6 @@ -0,0 +1,11 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-no-tracking-pane', { + name: 'time-tracking-no-tracking-pane', + template: ` + <div class='time-tracking-no-tracking-pane'> + <span class='no-value'>No estimate or time spent</span> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 new file mode 100644 index 00000000000..ed283fec3c3 --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/spent_only_pane.js.es6 @@ -0,0 +1,13 @@ +/* global Vue */ +(() => { + Vue.component('time-tracking-spent-only-pane', { + name: 'time-tracking-spent-only-pane', + props: ['timeSpentHumanReadable'], + template: ` + <div class='time-tracking-spend-only-pane'> + <span class='bold'>Spent:</span> + {{ timeSpentHumanReadable }} + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 new file mode 100644 index 00000000000..26563a7713b --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/components/time_tracker.js.es6 @@ -0,0 +1,118 @@ +/* global Vue */ +//= require ./help_state +//= require ./collapsed_state +//= require ./spent_only_pane +//= require ./no_tracking_pane +//= require ./estimate_only_pane +//= require ./comparison_pane + +(() => { + Vue.component('issuable-time-tracker', { + name: 'issuable-time-tracker', + props: [ + 'time_estimate', + 'time_spent', + 'human_time_estimate', + 'human_time_spent', + 'stopwatchSvg', + 'docsUrl', + ], + data() { + return { + showHelp: false, + }; + }, + computed: { + timeSpent() { + return this.time_spent; + }, + timeEstimate() { + return this.time_estimate; + }, + timeEstimateHumanReadable() { + return this.human_time_estimate; + }, + timeSpentHumanReadable() { + return this.human_time_spent; + }, + hasTimeSpent() { + return !!this.timeSpent; + }, + hasTimeEstimate() { + return !!this.timeEstimate; + }, + showComparisonState() { + return this.hasTimeEstimate && this.hasTimeSpent; + }, + showEstimateOnlyState() { + return this.hasTimeEstimate && !this.hasTimeSpent; + }, + showSpentOnlyState() { + return this.hasTimeSpent && !this.hasTimeEstimate; + }, + showNoTimeTrackingState() { + return !this.hasTimeEstimate && !this.hasTimeSpent; + }, + showHelpState() { + return !!this.showHelp; + }, + }, + methods: { + toggleHelpState(show) { + this.showHelp = show; + }, + }, + template: ` + <div class='time_tracker time-tracking-component-wrap' v-cloak> + <time-tracking-collapsed-state + :show-comparison-state='showComparisonState' + :show-help-state='showHelpState' + :show-spent-only-state='showSpentOnlyState' + :show-estimate-only-state='showEstimateOnlyState' + :time-spent-human-readable='timeSpentHumanReadable' + :time-estimate-human-readable='timeEstimateHumanReadable' + :stopwatch-svg='stopwatchSvg'> + </time-tracking-collapsed-state> + <div class='title hide-collapsed'> + Time tracking + <div class='help-button pull-right' + v-if='!showHelpState' + @click='toggleHelpState(true)'> + <i class='fa fa-question-circle'></i> + </div> + <div class='close-help-button pull-right' + v-if='showHelpState' + @click='toggleHelpState(false)'> + <i class='fa fa-close'></i> + </div> + </div> + <div class='time-tracking-content hide-collapsed'> + <time-tracking-estimate-only-pane + v-if='showEstimateOnlyState' + :time-estimate-human-readable='timeEstimateHumanReadable'> + </time-tracking-estimate-only-pane> + <time-tracking-spent-only-pane + v-if='showSpentOnlyState' + :time-spent-human-readable='timeSpentHumanReadable'> + </time-tracking-spent-only-pane> + <time-tracking-no-tracking-pane + v-if='showNoTimeTrackingState'> + </time-tracking-no-tracking-pane> + <time-tracking-comparison-pane + v-if='showComparisonState' + :time-estimate='timeEstimate' + :time-spent='timeSpent' + :time-spent-human-readable='timeSpentHumanReadable' + :time-estimate-human-readable='timeEstimateHumanReadable'> + </time-tracking-comparison-pane> + <transition name='help-state-toggle'> + <time-tracking-help-state + v-if='showHelpState' + :docs-url='docsUrl'> + </time-tracking-help-state> + </transition> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 new file mode 100644 index 00000000000..0b8da2b1f4f --- /dev/null +++ b/app/assets/javascripts/issuable/time_tracking/time_tracking_bundle.js.es6 @@ -0,0 +1,61 @@ +/* global Vue */ +//= require ./components/time_tracker +//= require smart_interval +//= require subbable_resource + +(() => { + /* This Vue instance represents what will become the parent instance for the + * sidebar. It will be responsible for managing `issuable` state and propagating + * changes to sidebar components. We will want to create a separate service to + * interface with the server at that point. + */ + + class IssuableTimeTracking { + constructor(issuableJSON) { + const parsedIssuable = JSON.parse(issuableJSON); + return this.initComponent(parsedIssuable); + } + + initComponent(parsedIssuable) { + this.parentInstance = new Vue({ + el: '#issuable-time-tracker', + data: { + issuable: parsedIssuable, + }, + methods: { + fetchIssuable() { + return gl.IssuableResource.get.call(gl.IssuableResource, { + type: 'GET', + url: gl.IssuableResource.endpoint, + }); + }, + updateState(data) { + this.issuable = data; + }, + subscribeToUpdates() { + gl.IssuableResource.subscribe(data => this.updateState(data)); + }, + listenForSlashCommands() { + $(document).on('ajax:success', '.gfm-form', (e, data) => { + const subscribedCommands = ['spend_time', 'time_estimate']; + const changedCommands = data.commands_changes; + + if (changedCommands && _.intersection(subscribedCommands, changedCommands).length) { + this.fetchIssuable(); + } + }); + }, + }, + created() { + this.fetchIssuable(); + }, + mounted() { + this.subscribeToUpdates(); + this.listenForSlashCommands(); + }, + }); + } + } + + gl.IssuableTimeTracking = IssuableTimeTracking; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 4aaad111082..c77fbb6a1c7 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,5 +1,7 @@ -/* 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, padded-blocks, max-len */ +/* 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 */ (function() { this.IssuableContext = (function() { @@ -37,6 +39,13 @@ }, 0); } }); + window.addEventListener('beforeunload', function() { + // collapsed_gutter cookie hides the sidebar + var bpBreakpoint = bp.getBreakpointSize(); + if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') { + Cookies.set('collapsed_gutter', true); + } + }); $(".right-sidebar").niceScroll(); } @@ -66,7 +75,5 @@ }; return IssuableContext; - })(); - }).call(this); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 1c4086517fe..293b856dc4d 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -1,11 +1,11 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, max-len */ /* global GitLab */ /* global UsersSelect */ /* global ZenMode */ /* global Autosave */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.IssuableForm = (function() { IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?'; @@ -51,7 +51,7 @@ IssuableForm.prototype.handleSubmit = function() { var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null; - if ((parseInt(fieldId) || 0) > 0) { + if ((parseInt(fieldId, 10) || 0) > 0) { if (!confirm(this.issueMoveConfirmMsg)) { return false; } @@ -150,7 +150,5 @@ }; return IssuableForm; - })(); - }).call(this); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 61e8531153b..081b0d8b0d7 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ /* global Flash */ /*= require flash */ @@ -6,7 +6,7 @@ /*= require task_list */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.Issue = (function() { function Issue() { @@ -151,7 +151,5 @@ }; return Issue; - })(); - }).call(this); diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js index b39d8274e13..1d6eff11403 100644 --- a/app/assets/javascripts/issue_status_select.js +++ b/app/assets/javascripts/issue_status_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ (function() { this.IssueStatusSelect = (function() { function IssueStatusSelect() { @@ -30,7 +30,5 @@ } return IssueStatusSelect; - })(); - }).call(this); diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6 index 52fd5d71b18..e0ebd36a65c 100644 --- a/app/assets/javascripts/issues_bulk_assignment.js.es6 +++ b/app/assets/javascripts/issues_bulk_assignment.js.es6 @@ -1,9 +1,8 @@ -/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, radix, max-len, padded-blocks, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ +/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ /* global Issuable */ /* global Flash */ ((global) => { - class IssuableBulkActions { constructor({ container, form, issues, prefixId } = {}) { this.prefixId = prefixId || 'issue_'; @@ -62,7 +61,6 @@ return labels; } - /** * Will return only labels that were marked previously and the user has unmarked * @return {Array} Label IDs @@ -81,7 +79,6 @@ return result; } - /** * Simple form serialization, it will return just what we need * Returns key/value pairs from form data @@ -163,5 +160,4 @@ } global.IssuableBulkActions = IssuableBulkActions; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6 index 33c5e35324d..8f48b1f57ce 100644 --- a/app/assets/javascripts/label_manager.js.es6 +++ b/app/assets/javascripts/label_manager.js.es6 @@ -1,8 +1,7 @@ -/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, padded-blocks, max-len */ +/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ /* global Flash */ ((global) => { - class LabelManager { constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority'); @@ -104,5 +103,4 @@ } gl.LabelManager = LabelManager; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js index 10de13c9a8a..40ad6fc348e 100644 --- a/app/assets/javascripts/labels.js +++ b/app/assets/javascripts/labels.js @@ -1,6 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, vars-on-top, no-unused-vars, max-len */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.Labels = (function() { function Labels() { @@ -42,7 +42,5 @@ }; return Labels; - })(); - }).call(this); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index ec2fc87bece..70dc0d06b7b 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -1,4 +1,4 @@ -/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks */ +/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread */ /* global Issuable */ /* global ListLabel */ @@ -333,10 +333,14 @@ if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) { $dropdown.parent() .find('.dropdown-clear-active') - .removeClass('is-active') + .removeClass('is-active'); } - if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { + if ($dropdown.hasClass('js-issuable-form-dropdown')) { + return; + } + + if ($dropdown.hasClass('js-filter-bulk-update')) { _this.enableBulkLabelDropdown(); _this.setDropdownData($dropdown, isMarking, this.id(label)); return; @@ -484,5 +488,4 @@ return LabelsSelect; })(); - }).call(this); diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index 2b700539c2b..1c0ea317c1a 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, indent, vars-on-top, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */ + (function() { var hideEndFade; @@ -27,10 +28,10 @@ }); $scrollingTabs.each(function () { - var $this = $(this), - scrollingTabWidth = $this.width(), - $active = $this.find('.active'), - activeWidth = $active.width(); + var $this = $(this); + var scrollingTabWidth = $this.width(); + var $active = $this.find('.active'); + var activeWidth = $active.width(); if ($active.length) { var offset = $active.offset().left + activeWidth; @@ -43,5 +44,4 @@ } }); }); - }).call(this); diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js index 4cdf99cae72..9cdc0309503 100644 --- a/app/assets/javascripts/lib/ace.js +++ b/app/assets/javascripts/lib/ace.js @@ -1,2 +1,3 @@ -/*= require ace-rails-ap */ +/*= require ace/ace */ /*= require ace/ext-searchbox */ +/*= require ./ace/ace_config_paths */ diff --git a/app/assets/javascripts/lib/ace/ace_config_paths.js.erb b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb new file mode 100644 index 00000000000..25e623f0fdc --- /dev/null +++ b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb @@ -0,0 +1,25 @@ +<% +ace_gem_path = Bundler.rubygems.find_name('ace-rails-ap').first.full_gem_path +ace_workers = Dir[ace_gem_path + '/vendor/assets/javascripts/ace/worker-*.js'].sort.map do |file| + File.basename(file, '.js').sub(/^worker-/, '') +end +ace_modes = Dir[ace_gem_path + '/vendor/assets/javascripts/ace/mode-*.js'].sort.map do |file| + File.basename(file, '.js').sub(/^mode-/, '') +end +%> + +(function() { + window.gon = window.gon || {}; + var basePath = (window.gon.relative_url_root || '').replace(/\/$/, '') + '/assets/ace'; + ace.config.set('basePath', basePath); + + // configure paths for all worker modules +<% ace_workers.each do |worker| %> + ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/worker-<%= worker %>.js'); +<% end %> + + // configure paths for all mode modules +<% ace_modes.each do |mode| %> + ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/mode-<%= mode %>.js'); +<% end %> +})(); diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js index 83957af94f3..ce090a2e4fd 100644 --- a/app/assets/javascripts/lib/utils/animate.js +++ b/app/assets/javascripts/lib/utils/animate.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, max-len */ (function() { (function(w) { if (w.gl == null) { @@ -46,5 +46,4 @@ return dfd.promise(); }; })(window); - }).call(this); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 0a0e73e0ccc..51993bb3420 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, padded-blocks, max-len, prefer-template */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-param-reassign, no-else-return, quotes, object-shorthand, comma-dangle, camelcase, one-var, vars-on-top, one-var-declaration-per-line, no-return-assign, consistent-return, max-len, prefer-template */ (function() { (function(w) { var base; @@ -106,8 +106,9 @@ ); }; - gl.utils.getPagePath = function() { - return $('body').data('page').split(':')[0]; + gl.utils.getPagePath = function(index) { + index = index || 0; + return $('body').data('page').split(':')[index]; }; gl.utils.parseUrl = function (url) { @@ -123,10 +124,110 @@ return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; }; + gl.utils.getUrlParamsArray = function () { + // We can trust that each param has one & since values containing & will be encoded + // Remove the first character of search as it is always ? + return window.location.search.slice(1).split('&'); + }; + gl.utils.isMetaKey = function(e) { return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; - })(window); + gl.utils.scrollToElement = function($el) { + var top = $el.offset().top; + gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height(); + gl.navLinksHeight = gl.navLinksHeight || $('.nav-links').height(); + gl.mrTabsHeight = gl.mrTabsHeight || $('.merge-request-tabs').height(); + + return $('body, html').animate({ + scrollTop: top - (gl.navBarHeight + gl.navLinksHeight + gl.mrTabsHeight) + }, 200); + }; + + /** + this will take in the `name` of the param you want to parse in the url + if the name does not exist this function will return `null` + otherwise it will return the value of the param key provided + */ + w.gl.utils.getParameterByName = (name) => { + const url = window.location.href; + name = name.replace(/[[\]]/g, '\\$&'); + const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); + const results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); + }; + + w.gl.utils.getSelectedFragment = () => { + const selection = window.getSelection(); + const documentFragment = selection.getRangeAt(0).cloneContents(); + if (documentFragment.textContent.length === 0) return null; + + return documentFragment; + }; + + w.gl.utils.insertText = (target, text) => { + // Firefox doesn't support `document.execCommand('insertText', false, text)` on textareas + + const selectionStart = target.selectionStart; + const selectionEnd = target.selectionEnd; + const value = target.value; + + const textBefore = value.substring(0, selectionStart); + const textAfter = value.substring(selectionEnd, value.length); + const newText = textBefore + text + textAfter; + + target.value = newText; + target.selectionStart = target.selectionEnd = selectionStart + text.length; + + // Trigger autosave + $(target).trigger('input'); + // Trigger autosize + var event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + target.dispatchEvent(event); + }; + + w.gl.utils.nodeMatchesSelector = (node, selector) => { + const matches = Element.prototype.matches || + Element.prototype.matchesSelector || + Element.prototype.mozMatchesSelector || + Element.prototype.msMatchesSelector || + Element.prototype.oMatchesSelector || + Element.prototype.webkitMatchesSelector; + + if (matches) { + return matches.call(node, selector); + } + + // IE11 doesn't support `node.matches(selector)` + + let parentNode = node.parentNode; + if (!parentNode) { + parentNode = document.createElement('div'); + node = node.cloneNode(true); + parentNode.appendChild(node); + } + + const matchingNodes = parentNode.querySelectorAll(selector); + return Array.prototype.indexOf.call(matchingNodes, node) !== -1; + }; + + /** + this will take in the headers from an API response and normalize them + this way we don't run into production issues when nginx gives us lowercased header keys + */ + w.gl.utils.normalizeHeaders = (headers) => { + const upperCaseHeaders = {}; + + Object.keys(headers).forEach((e) => { + upperCaseHeaders[e.toUpperCase()] = headers[e]; + }); + + return upperCaseHeaders; + }; + })(window); }).call(this); diff --git a/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 b/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 deleted file mode 100644 index 5ae978010c9..00000000000 --- a/app/assets/javascripts/lib/utils/custom_event_polyfill.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -/** - * CustomEvent support for IE - */ -if (typeof window.CustomEvent !== 'function') { - window.CustomEvent = function CustomEvent(e, params) { - const options = params || { bubbles: false, cancelable: false, detail: undefined }; - const evt = document.createEvent('CustomEvent'); - evt.initCustomEvent(e, options.bubbles, options.cancelable, options.detail); - return evt; - }; - window.CustomEvent.prototype = window.Event.prototype; -} diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index e8e502694d6..3ed8bfd5651 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */ /* global timeago */ /* global dateFormat */ @@ -97,7 +97,5 @@ return Math.floor((date2 - date1) / millisecondsPerDay); }; - })(window); - }).call(this); diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js index 3c9ad0e67c8..6d5979603b9 100644 --- a/app/assets/javascripts/lib/utils/notify.js +++ b/app/assets/javascripts/lib/utils/notify.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, max-len */ (function() { (function(w) { @@ -44,5 +44,4 @@ w.notify = notifyMe; return w.notifyPermissions = notifyPermissions; })(window); - }).call(this); diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js.es6 index ccaf447eb0b..ae397212e55 100644 --- a/app/assets/javascripts/lib/utils/pretty_time.js.es6 +++ b/app/assets/javascripts/lib/utils/pretty_time.js.es6 @@ -4,13 +4,13 @@ * stringifyTime condensed or non-condensed, abbreviateTimelengths) * */ - class PrettyTime { - + const utils = window.gl.utils = gl.utils || {}; + const prettyTime = utils.prettyTime = { /* * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } * Seconds can be negative or positive, zero or non-zero. */ - static parseSeconds(seconds) { + parseSeconds(seconds) { const DAYS_PER_WEEK = 5; const HOURS_PER_DAY = 8; const MINUTES_PER_HOUR = 60; @@ -24,7 +24,7 @@ minutes: 1, }; - let unorderedMinutes = PrettyTime.secondsToMinutes(seconds); + let unorderedMinutes = prettyTime.secondsToMinutes(seconds); return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); @@ -33,35 +33,33 @@ return periodCount; }); - } + }, /* * Accepts a timeObject and returns a condensed string representation of it * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. */ - static stringifyTime(timeObject) { + stringifyTime(timeObject) { const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { const isNonZero = !!unitValue; return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; }, '').trim(); return reducedTime.length ? reducedTime : '0m'; - } + }, /* * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns * the first non-zero unit/value pair. */ - static abbreviateTime(timeStr) { + abbreviateTime(timeStr) { return timeStr.split(' ') .filter(unitStr => unitStr.charAt(0) !== '0')[0]; - } + }, - static secondsToMinutes(seconds) { + secondsToMinutes(seconds) { return Math.abs(seconds / 60); - } - } - - gl.PrettyTime = PrettyTime; + }, + }; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index ac44b81ee22..6bb575059b7 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,4 +1,5 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, semi, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */ + (function() { (function(w) { var base; @@ -10,13 +11,28 @@ } gl.text.addDelimiter = function(text) { return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text; - } + }; gl.text.randomString = function() { return Math.random().toString(36).substring(7); }; gl.text.replaceRange = function(s, start, end, substitute) { return s.substring(0, start) + substitute + s.substring(end); }; + gl.text.getTextWidth = function(text, font) { + /** + * Uses canvas.measureText to compute and return the width of the given text of given font in pixels. + * + * @param {String} text The text to be rendered. + * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana"). + * + * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 + */ + // re-use canvas object for better performance + var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas')); + var context = canvas.getContext('2d'); + context.font = font; + return context.measureText(text).width; + }; gl.text.selectedText = function(text, textarea) { return text.substring(textarea.selectionStart, textarea.selectionEnd); }; @@ -44,9 +60,25 @@ } }; gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) { - var insertText, inserted, selectedSplit, startChar; + var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine; + removedLastNewLine = false; + removedFirstNewLine = false; + + // Remove the first newline + if (selected.indexOf('\n') === 0) { + removedFirstNewLine = true; + selected = selected.replace(/\n+/, ''); + } + + // Remove the last newline + if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) { + removedLastNewLine = true; + selected = selected.replace(/\n$/, ''); + } + selectedSplit = selected.split('\n'); startChar = !wrap && textArea.selectionStart > 0 ? '\n' : ''; + if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) { if (blockTag != null) { insertText = this.blockTagText(text, textArea, blockTag, selected); @@ -62,6 +94,15 @@ } else { insertText = "" + startChar + tag + selected + (wrap ? tag : ' '); } + + if (removedFirstNewLine) { + insertText = '\n' + insertText; + } + + if (removedLastNewLine) { + insertText += '\n'; + } + if (document.queryCommandSupported('insertText')) { inserted = document.execCommand('insertText', false, insertText); } @@ -74,9 +115,9 @@ document.execCommand("ms-endUndoUnit"); } catch (error) {} } - return this.moveCursor(textArea, tag, wrap); + return this.moveCursor(textArea, tag, wrap, removedLastNewLine); }; - gl.text.moveCursor = function(textArea, tag, wrapped) { + gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) { var pos; if (!textArea.setSelectionRange) { return; @@ -87,6 +128,11 @@ } else { pos = textArea.selectionStart; } + + if (removedLastNewLine) { + pos -= 1; + } + return textArea.setSelectionRange(pos, pos); } }; @@ -114,10 +160,9 @@ }; gl.text.humanize = function(string) { return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); - } + }; return gl.text.truncate = function(string, maxLength) { return string.substr(0, (maxLength - 3)) + '...'; }; })(window); - }).call(this); diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js index 961859dfb5b..6d813d61601 100644 --- a/app/assets/javascripts/lib/utils/type_utility.js +++ b/app/assets/javascripts/lib/utils/type_utility.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-return-assign, max-len */ (function() { (function(w) { var base; @@ -12,5 +12,4 @@ return (obj != null) && (obj.constructor === Object); }; })(window); - }).call(this); diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 6872186cd7f..8e15bf0735c 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, no-plusplus, guard-for-in, no-restricted-syntax, prefer-template, quotes, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ (function() { (function(w) { var base; @@ -22,7 +22,7 @@ if (sParameterName[0] === sParam) { values.push(sParameterName[1].replace(/\+/g, ' ')); } - i++; + i += 1; } return values; }; @@ -57,7 +57,7 @@ return ((function() { var j, len, results; results = []; - for (j = 0, len = urlVariables.length; j < len; j++) { + for (j = 0, len = urlVariables.length; j < len; j += 1) { variables = urlVariables[j]; if (variables.indexOf(param) === -1) { results.push(variables); @@ -77,5 +77,4 @@ return hashIndex === -1 ? null : url.substring(hashIndex + 1); }; })(window); - }).call(this); diff --git a/app/assets/javascripts/lib/vue_resource.js.es6 b/app/assets/javascripts/lib/vue_resource.js.es6 new file mode 100644 index 00000000000..eff1dcabfa2 --- /dev/null +++ b/app/assets/javascripts/lib/vue_resource.js.es6 @@ -0,0 +1,2 @@ +//= require vue +//= require vue-resource diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 9af89b79f84..2f147704c22 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-else-return, max-len */ // LineHighlighter // @@ -31,7 +31,7 @@ // </div> // (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.LineHighlighter = (function() { // CSS class applied to highlighted lines @@ -74,8 +74,9 @@ // If not done this way, the line number anchor will sometimes keep its // active state even when the event is cancelled, resulting in an ugly border // around the link and/or a persisted underline text decoration. - return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) { - return event.preventDefault(); + $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) { + event.preventDefault(); + event.stopPropagation(); }); }; @@ -119,11 +120,11 @@ // Returns an Array LineHighlighter.prototype.hashToRange = function(hash) { var first, last, matches; - //?L(\d+)(?:-(\d+))?$/) + // ?L(\d+)(?:-(\d+))?$/) matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/); if (matches && matches.length) { - first = parseInt(matches[1]); - last = matches[2] ? parseInt(matches[2]) : null; + first = parseInt(matches[1], 10); + last = matches[2] ? parseInt(matches[2], 10) : null; return [first, last]; } else { return [null, null]; @@ -144,7 +145,7 @@ var i, lineNumber, ref, ref1, results; if (range[1]) { results = []; - for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? ++i : --i) { + for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) { results.push(this.highlightLine(lineNumber)); } return results; @@ -178,7 +179,5 @@ }; return LineHighlighter; - })(); - }).call(this); diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index 0ae6df311bb..ea9bfb4860a 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */ /* global Turbolinks */ (function() { @@ -11,5 +11,4 @@ $(document).on('page:change', function() { $('.tanuki-logo').removeClass('animate'); }); - }).call(this); diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js.es6 index 7741cd29793..bf6c0ec2798 100644 --- a/app/assets/javascripts/member_expiration_date.js +++ b/app/assets/javascripts/member_expiration_date.js.es6 @@ -1,30 +1,29 @@ -/* eslint-disable func-names, space-before-function-paren, vars-on-top, no-var, object-shorthand, comma-dangle, max-len */ -(function() { +(() => { // Add datepickers to all `js-access-expiration-date` elements. If those elements are // children of an element with the `clearable-input` class, and have a sibling // `js-clear-input` element, then show that element when there is a value in the // datepicker, and make clicking on that element clear the field. // - gl.MemberExpirationDate = function() { + window.gl = window.gl || {}; + gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => { function toggleClearInput() { $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); } - - var inputs = $('.js-access-expiration-date'); + const inputs = $(selector); inputs.datepicker({ dateFormat: 'yy-mm-dd', minDate: 1, - onSelect: function () { + onSelect: function onSelect() { $(this).trigger('change'); toggleClearInput.call(this); - } + }, }); - inputs.next('.js-clear-input').on('click', function(event) { + inputs.next('.js-clear-input').on('click', function clicked(event) { event.preventDefault(); - var input = $(this).closest('.clearable-input').find('.js-access-expiration-date'); + const input = $(this).closest('.clearable-input').find(selector); input.datepicker('setDate', null) .trigger('change'); toggleClearInput.call(input); diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 index f95b079c972..c7e78fed8fe 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -1,10 +1,9 @@ -/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, prefer-const, no-new, padded-blocks, no-param-reassign, semi, max-len */ +/* 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 */ ((global) => { - global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.diffFileEditor = Vue.extend({ @@ -19,7 +18,7 @@ loading: false, fileLoaded: false, originalContent: '', - } + }; }, computed: { classObject() { @@ -51,8 +50,8 @@ $.get(this.file.content_path) .done((file) => { - let content = this.$el.querySelector('pre'); - let fileContent = document.createTextNode(file.content); + const content = this.$el.querySelector('pre'); + const fileContent = document.createTextNode(file.content); content.textContent = fileContent.textContent; @@ -94,5 +93,4 @@ } } }); - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 index 74544b7d0c7..240c8f98932 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 @@ -1,8 +1,7 @@ -/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* eslint-disable no-param-reassign, comma-dangle */ /* global Vue */ ((global) => { - global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.inlineConflictLines = Vue.extend({ @@ -11,5 +10,4 @@ }, mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], }); - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 index 78c00c31c16..97753c50b60 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 @@ -1,8 +1,7 @@ -/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ +/* eslint-disable no-param-reassign, comma-dangle */ /* global Vue */ ((global) => { - global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.parallelConflictLines = Vue.extend({ @@ -26,5 +25,4 @@ </table> `, }); - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 index 8df3170edac..c012b77e0bf 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-param-reassign, comma-dangle, no-extra-semi, padded-blocks */ +/* eslint-disable no-param-reassign, comma-dangle */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -25,8 +25,7 @@ method: 'POST' }); } - }; + } global.mergeConflicts.mergeConflictsService = mergeConflictsService; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index 53b44007510..74587df22c5 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, object-shorthand, no-dupe-keys, no-param-reassign, no-plusplus, camelcase, prefer-const, no-nested-ternary, no-continue, semi, func-call-spacing, no-spaced-func, padded-blocks, max-len */ +/* eslint-disable comma-dangle, object-shorthand, no-param-reassign, camelcase, no-nested-ternary, no-continue, max-len */ /* global Cookies */ /* global Vue */ @@ -40,7 +40,6 @@ commitMessage: data.commit_message, sourceBranch: data.source_branch, targetBranch: data.target_branch, - commitMessage: data.commit_message, shortCommitSha: data.commit_sha.slice(0, 7), }; }, @@ -89,7 +88,7 @@ this.decorateLineForInlineView(line, id, conflict); file.inlineLines.push(line); - }) + }); if (conflict) { file.inlineLines.push(this.getOriginHeaderLine(id)); @@ -121,7 +120,7 @@ } else { const lineType = type || 'context'; - linesObj.left.push (this.getLineForParallelView(line, id, lineType)); + linesObj.left.push(this.getLineForParallelView(line, id, lineType)); linesObj.right.push(this.getLineForParallelView(line, id, lineType, true)); } }); @@ -129,7 +128,7 @@ this.checkLineLengths(linesObj); }); - for (let i = 0, len = linesObj.left.length; i < len; i++) { + for (let i = 0, len = linesObj.left.length; i < len; i += 1) { file.parallelLines.push([ linesObj.right[i], linesObj.left[i] @@ -162,11 +161,11 @@ if (file.type === CONFLICT_TYPES.TEXT) { file.sections.forEach((section) => { if (section.conflict) { - count++; + count += 1; } }); } else { - count++; + count += 1; } }); @@ -252,17 +251,17 @@ }, checkLineLengths(linesObj) { - let { left, right } = linesObj; + const { left, right } = linesObj; if (left.length !== right.length) { if (left.length > right.length) { const diff = left.length - right.length; - for (let i = 0; i < diff; i++) { + for (let i = 0; i < diff; i += 1) { right.push({ lineType: 'emptyLine', richText: '' }); } } else { const diff = right.length - left.length; - for (let i = 0; i < diff; i++) { + for (let i = 0; i < diff; i += 1) { left.push({ lineType: 'emptyLine', richText: '' }); } } @@ -316,32 +315,31 @@ const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length; let unresolved = 0; - for (let i = 0, l = files.length; i < l; i++) { - let file = files[i]; + for (let i = 0, l = files.length; i < l; i += 1) { + const file = files[i]; if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) { let numberConflicts = 0; - let resolvedConflicts = Object.keys(file.resolutionData).length + const resolvedConflicts = Object.keys(file.resolutionData).length; // We only check for conflicts type 'text' // since conflicts `text_editor` can´t be resolved in interactive mode if (file.type === CONFLICT_TYPES.TEXT) { - for (let j = 0, k = file.sections.length; j < k; j++) { + for (let j = 0, k = file.sections.length; j < k; j += 1) { if (file.sections[j].conflict) { - numberConflicts++; + numberConflicts += 1; } } if (resolvedConflicts !== numberConflicts) { - unresolved++; + unresolved += 1; } } } else if (file.resolveMode === EDIT_RESOLVE_MODE) { - // Unlikely to happen since switching to Edit mode saves content automatically. // Checking anyway in case the save strategy changes in the future if (!file.content) { - unresolved++; + unresolved += 1; continue; } } @@ -366,15 +364,12 @@ }; this.state.conflictsData.files.forEach((file) => { - let addFile; - - addFile = { + const addFile = { old_path: file.old_path, new_path: file.new_path }; if (file.type === CONFLICT_TYPES.TEXT) { - // Submit only one data for type of editing if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) { addFile.sections = file.resolutionData; @@ -435,5 +430,4 @@ return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT); } }; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 index 83520702f9b..a2d90f9ba47 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable new-cap, comma-dangle, no-new, semi */ +/* eslint-disable new-cap, comma-dangle, no-new */ /* global Vue */ /* global Flash */ @@ -29,10 +29,10 @@ $(() => { 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines }, computed: { - conflictsCountText() { return mergeConflictsStore.getConflictsCountText() }, - readyToCommit() { return mergeConflictsStore.isReadyToCommit() }, - commitButtonText() { return mergeConflictsStore.getCommitButtonText() }, - showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent() } + conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); }, + readyToCommit() { return mergeConflictsStore.isReadyToCommit(); }, + commitButtonText() { return mergeConflictsStore.getCommitButtonText(); }, + showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); } }, created() { mergeConflictsService @@ -88,5 +88,5 @@ $(() => { }); } } - }) + }); }); diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 index e89b35d5407..53e000d7e9e 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-param-reassign, comma-dangle, padded-blocks */ +/* eslint-disable no-param-reassign, comma-dangle */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -10,5 +10,4 @@ } } }; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 index a4aca85d460..0f475f62ee6 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-param-reassign, quote-props, comma-dangle, padded-blocks */ +/* eslint-disable no-param-reassign, quote-props, comma-dangle */ ((global) => { global.mergeConflicts = global.mergeConflicts || {}; @@ -16,5 +16,4 @@ } } }; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 244c2f6746c..09ee8dbe9d7 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */ /* global MergeRequestTabs */ /*= require jquery.waitforimages */ @@ -6,7 +6,7 @@ /*= require merge_request_tabs */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.MergeRequest = (function() { function MergeRequest(opts) { @@ -130,7 +130,5 @@ }; return MergeRequest; - })(); - }).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 860e7e066a0..4c8c28af755 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -237,13 +237,8 @@ } this.diffsLoaded = true; - const diffPage = new gl.Diff(); - - const locationHash = gl.utils.getLocationHash(); - const anchoredDiff = locationHash && locationHash.split('_')[0]; - if (anchoredDiff) { - diffPage.openAnchoredDiff(anchoredDiff, () => this.scrollToElement('#diffs')); - } + new gl.Diff(); + this.scrollToElement('#diffs'); }, }); } diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 0305aeb07d9..7cc319e2f4e 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -1,11 +1,11 @@ -/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, no-plusplus, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, template-curly-spacing, camelcase, default-case, wrap-iife, semi, padded-blocks */ +/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, camelcase, default-case, wrap-iife */ /* global notify */ /* global notifyPermissions */ /* global merge_request_widget */ /* global Turbolinks */ ((global) => { - var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>"> <div class="ci_widget ci-success"> @@ -90,7 +90,7 @@ const $ciSuccessIcon = $('.js-success-icon'); this.$ciSuccessIcon = $ciSuccessIcon.html(); $ciSuccessIcon.remove(); - } + }; MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { if (deleteSourceBranch == null) { @@ -126,7 +126,9 @@ MergeRequestWidget.prototype.getMergeStatus = function() { return $.get(this.opts.merge_check_url, function(data) { - return $('.mr-state-widget').replaceWith(data); + var $html = $(data); + $('.mr-widget-body').replaceWith($html.find('.mr-widget-body')); + $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer')); }); }; @@ -187,9 +189,9 @@ }; MergeRequestWidget.prototype.renderEnvironments = function(environments) { - for (let i = 0; i < environments.length; i++) { + for (let i = 0; i < environments.length; i += 1) { const environment = environments[i]; - if ($(`.mr-state-widget #${ environment.id }`).length) return; + if ($(`.mr-state-widget #${environment.id}`).length) return; const $template = $(DEPLOYMENT_TEMPLATE); if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); @@ -205,7 +207,7 @@ } environment.ci_success_icon = this.$ciSuccessIcon; const templateString = _.unescape($template[0].outerHTML); - const template = _.template(templateString)(environment) + const template = _.template(templateString)(environment); this.$widgetBody.before(template); } }; @@ -247,7 +249,5 @@ }; return MergeRequestWidget; - })(); - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 index 2b074994b4a..5969d2ba56b 100644 --- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 @@ -8,31 +8,42 @@ * temporarily. * */ - if ($('.accept-mr-form').length) { - $('.accept-mr-form').on('ajax:send', () => { - $('.accept-mr-form :input').disable(); - }); + $(document) + .off('ajax:send', '.accept-mr-form') + .on('ajax:send', '.accept-mr-form', () => { + $('.accept-mr-form :input').disable(); + }); - $('.accept_merge_request').on('click', () => { - $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); - }); + $(document) + .off('click', '.accept_merge_request') + .on('click', '.accept_merge_request', () => { + $('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress'); + }); - $('.merge_when_build_succeeds').on('click', () => { - $('#merge_when_build_succeeds').val('1'); - }); + $(document) + .off('click', '.merge_when_build_succeeds') + .on('click', '.merge_when_build_succeeds', () => { + $('#merge_when_build_succeeds').val('1'); + }); - $('.js-merge-dropdown a').on('click', (e) => { - e.preventDefault(); - $(this).closest('form').submit(); - }); - } else if ($('.rebase-in-progress').length) { + $(document) + .off('click', '.js-merge-dropdown a') + .on('click', '.js-merge-dropdown a', (e) => { + e.preventDefault(); + $(e.target).closest('form').submit(); + }); + if ($('.rebase-in-progress').length) { merge_request_widget.rebaseInProgress(); } else if ($('.rebase-mr-form').length) { - $('.rebase-mr-form').on('ajax:send', () => { + $(document) + .off('ajax:send', '.rebase-mr-form') + .on('ajax:send', '.rebase-mr-form', () => { $('.rebase-mr-form :input').disable(); }); - $('.js-rebase-button').on('click', () => { + $(document) + .off('click', '.js-rebase-button') + .on('click', '.js-rebase-button', () => { $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress"); }); } else { diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js index 9f8af46c715..527cdc9b698 100644 --- a/app/assets/javascripts/merged_buttons.js +++ b/app/assets/javascripts/merged_buttons.js @@ -1,7 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.MergedButtons = (function() { function MergedButtons() { @@ -41,7 +41,5 @@ }; return MergedButtons; - })(); - }).call(this); diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 42152362e60..7ce1259e015 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, max-len */ /* global Flash */ (function() { @@ -193,7 +193,5 @@ }; return Milestone; - })(); - }).call(this); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 28054b78249..7ab39ffbd05 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -1,4 +1,4 @@ -/* 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, padded-blocks */ +/* 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 */ @@ -181,7 +181,5 @@ } return MilestoneSelect; - })(); - }).call(this); diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 index 90b3366f14b..80549532ea9 100644 --- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -10,9 +10,9 @@ * The container should be the table element. * * The stage icon clicked needs to have the following HTML structure: - * <div> - * <button class="dropdown js-builds-dropdown-button"></button> - * <div class="js-builds-dropdown-container"></div> + * <div class="dropdown"> + * <button class="dropdown js-builds-dropdown-button" data-toggle="dropdown"></button> + * <div class="js-builds-dropdown-container dropdown-menu"></div> * </div> */ (() => { @@ -26,13 +26,11 @@ } /** - * Adds and removes the event listener. + * Adds the event listener when the dropdown is opened. + * All dropdown events are fired at the .dropdown-menu's parent element. */ bindEvents() { - const dropdownButtonSelector = 'button.js-builds-dropdown-button'; - - $(this.container).off('click', dropdownButtonSelector, this.getBuildsList) - .on('click', dropdownButtonSelector, this.getBuildsList); + $(this.container).on('shown.bs.dropdown', this.getBuildsList); } /** @@ -52,11 +50,14 @@ /** * For the clicked stage, gets the list of builds. * - * @param {Object} e + * All dropdown events have a relatedTarget property, + * whose value is the toggling anchor element. + * + * @param {Object} e bootstrap dropdown event * @return {Promise} */ getBuildsList(e) { - const button = e.currentTarget; + const button = e.relatedTarget; const endpoint = button.dataset.stageEndpoint; return $.ajax({ diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 6633f2c2709..514556ade0b 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,8 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */ /* global Api */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; window.NamespaceSelect = (function() { function NamespaceSelect(opts) { @@ -63,7 +63,6 @@ }; return NamespaceSelect; - })(); window.NamespaceSelects = (function() { @@ -83,7 +82,5 @@ } return NamespaceSelects; - })(); - }).call(this); diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 20a68780cd5..a7ccd03b60c 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -1,8 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len */ /* global Raphael */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.BranchGraph = (function() { function BranchGraph(element1, options1) { @@ -53,7 +53,7 @@ this.top = this.r.set(); this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320); ref = this.commits; - for (j = 0, len = ref.length; j < len; j++) { + for (j = 0, len = ref.length; j < len; j += 1) { c = ref[j]; if (c.id in this.parents) { c.isParent = true; @@ -68,7 +68,7 @@ var c, j, len, p, ref, results; ref = this.commits; results = []; - for (j = 0, len = ref.length; j < len; j++) { + for (j = 0, len = ref.length; j < len; j += 1) { c = ref[j]; this.mtime = Math.max(this.mtime, c.time); this.mspace = Math.max(this.mspace, c.space); @@ -76,7 +76,7 @@ var l, len1, ref1, results1; ref1 = c.parents; results1 = []; - for (l = 0, len1 = ref1.length; l < len1; l++) { + for (l = 0, len1 = ref1.length; l < len1; l += 1) { p = ref1[l]; this.parents[p[0]] = true; results1.push(this.mspace = Math.max(this.mspace, p[1])); @@ -96,7 +96,7 @@ // Skipping a few colors in the spectrum to get more contrast between colors Raphael.getColor(); Raphael.getColor(); - results.push(k++); + results.push(k += 1); } return results; }; @@ -113,7 +113,7 @@ fill: "#444" }); ref = this.days; - for (mm = j = 0, len = ref.length; j < len; mm = ++j) { + for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) { day = ref[mm]; if (cuday !== day[0] || cumonth !== day[1]) { // Dates @@ -286,7 +286,7 @@ r = this.r; ref = commit.parents; results = []; - for (i = j = 0, len = ref.length; j < len; i = ++j) { + for (i = j = 0, len = ref.length; j < len; i = (j += 1)) { parent = ref[i]; parentCommit = this.preparedCommits[parent[0]]; parentY = this.offsetY + this.unitTime * parentCommit.time; @@ -346,7 +346,6 @@ }; return BranchGraph; - })(); Raphael.prototype.commitTooltip = function(x, y, commit) { @@ -399,7 +398,7 @@ words = content.split(" "); x = 0; s = []; - for (j = 0, len = words.length; j < len; j++) { + for (j = 0, len = words.length; j < len; j += 1) { word = words[j]; if (x + (word.length * letterWidth) > width) { s.push("\n"); @@ -422,5 +421,4 @@ y: h }); }; - }).call(this); diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js index 2367d2497b2..37bf6436fd1 100644 --- a/app/assets/javascripts/network/network.js +++ b/app/assets/javascripts/network/network.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */ /* global BranchGraph */ (function() { @@ -16,7 +16,5 @@ } return Network; - })(); - }).call(this); diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 17833d3419a..2e6eb83cec7 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */ /* global Network */ /* global ShortcutsNetwork */ @@ -23,5 +23,4 @@ }); return new ShortcutsNetwork(network_graph.branch_graph); }); - }).call(this); diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 29a323dd4c6..7f763c13b50 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,7 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, one-var, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, - indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; this.NewBranchForm = (function() { function NewBranchForm(form, availableRefs) { @@ -99,7 +99,5 @@ }; return NewBranchForm; - })(); - }).call(this); diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js index 8fb8f3e4a5f..41eea78a3e6 100644 --- a/app/assets/javascripts/new_commit_form.js +++ b/app/assets/javascripts/new_commit_form.js @@ -1,6 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-return-assign, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-return-assign, max-len */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.NewCommitForm = (function() { function NewCommitForm(form) { @@ -29,7 +29,5 @@ }; return NewCommitForm; - })(); - }).call(this); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index a8b9a352870..9db830a7ada 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -1,8 +1,9 @@ -/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, 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, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks */ +/* 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 GLForm */ /* global Autosave */ /* global ResolveService */ +/* global mrRefreshWidgetUrl */ /*= require autosave */ /*= require autosize */ @@ -13,7 +14,7 @@ /*= require task_list */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.Notes = (function() { const MAX_VISIBLE_COMMIT_LIST_COUNT = 3; @@ -52,6 +53,12 @@ this.setupMainTargetNoteForm(); this.initTaskList(); this.collapseLongCommitList(); + + // We are in the Merge Requests page so we need another edit form for Changes tab + if (gl.utils.getPagePath(1) === 'merge_requests') { + $('.note-edit-form').clone() + .addClass('mr-note-edit-form').insertAfter('.note-edit-form'); + } } Notes.prototype.addBinding = function() { @@ -63,7 +70,7 @@ // change note in UI after update $(document).on("ajax:success", "form.edit-note", this.updateNote); // Edit note link - $(document).on("click", ".js-note-edit", this.showEditForm); + $(document).on("click", ".js-note-edit", this.showEditForm.bind(this)); $(document).on("click", ".note-edit-cancel", this.cancelEdit); // Reopen and close actions for Issue/MR combined with note form submit $(document).on("click", ".js-comment-button", this.updateCloseButton); @@ -213,7 +220,6 @@ })(this)); }; - /* Increase @pollingInterval up to 120 seconds on every function call, if `shouldReset` has a truthy value, 'null' or 'undefined' the variable @@ -237,6 +243,15 @@ return this.initRefresh(); }; + Notes.prototype.handleCreateChanges = function(note) { + if (typeof note === 'undefined') { + return; + } + + if (note.commands_changes && note.commands_changes.indexOf('merge') !== -1) { + $.get(mrRefreshWidgetUrl); + } + }; /* Render note in main comments area. @@ -277,7 +292,6 @@ } }; - /* Check if note does not exists on page */ @@ -290,7 +304,6 @@ return this.view === 'parallel'; }; - /* Render note in discussion area. @@ -341,7 +354,6 @@ return this.updateNotesCount(1); }; - /* Called in response the main target form has been successfully submitted. @@ -373,7 +385,6 @@ return form.find(".js-note-text").trigger("input"); }; - /* Shows the main form and does some setup on it. @@ -398,7 +409,6 @@ return this.parentTimeline = form.parents('.timeline'); }; - /* General note form setup. @@ -415,7 +425,6 @@ return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]); }; - /* Called in response to the new note form being submitted @@ -423,6 +432,7 @@ */ Notes.prototype.addNote = function(xhr, note, status) { + this.handleCreateChanges(note); return this.renderNote(note); }; @@ -430,7 +440,6 @@ return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline); }; - /* Called in response to the new note form being submitted @@ -455,7 +464,6 @@ this.removeDiscussionNoteForm($form); }; - /* Called in response to the edit note form being submitted @@ -466,6 +474,7 @@ var $html, $note_li; // Convert returned HTML to a jQuery object so we can modify it further $html = $(note.html); + this.revertNoteEditForm(); gl.utils.localTimeAgo($('.js-timeago', $html)); $html.renderGFM(); $html.find('.js-task-list-container').taskList('enable'); @@ -479,51 +488,56 @@ } }; + Notes.prototype.checkContentToAllowEditing = function($el) { + var initialContent = $el.find('.original-note-content').text().trim(); + var currentContent = $el.find('.note-textarea').val(); + var isAllowed = true; + + if (currentContent === initialContent) { + this.removeNoteEditForm($el); + } + else { + var $buttons = $el.find('.note-form-actions'); + var isWidgetVisible = gl.utils.isInViewport($el.get(0)); + + if (!isWidgetVisible) { + gl.utils.scrollToElement($el); + } + + $el.find('.js-edit-warning').show(); + isAllowed = false; + } + + return isAllowed; + }; /* Called in response to clicking the edit note link Replaces the note text with the note edit form Adds a data attribute to the form with the original content of the note for cancellations - */ - + */ Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) { - var $noteText, done, form, note; e.preventDefault(); - note = $(this).closest(".note"); - note.addClass("is-editting"); - form = note.find(".note-edit-form"); - form.addClass('current-note-edit-form'); - // Show the attachment delete link - note.find(".js-note-attachment-delete").show(); - done = function($noteText) { - var noteTextVal; - // Neat little trick to put the cursor at the end - noteTextVal = $noteText.val(); - // Store the original note text in a data attribute to retrieve if a user cancels edit. - form.find('form.edit-note').data('original-note', noteTextVal); - return $noteText.val('').val(noteTextVal); - }; - new GLForm(form); - if ((scrollTo != null) && (myLastNote != null)) { - // scroll to the bottom - // so the open of the last element doesn't make a jump - $('html, body').scrollTop($(document).height()); - return $('html, body').animate({ - scrollTop: myLastNote.offset().top - 150 - }, 500, function() { - var $noteText; - $noteText = form.find(".js-note-text"); - $noteText.focus(); - return done($noteText); - }); - } else { - $noteText = form.find('.js-note-text'); - $noteText.focus(); - return done($noteText); + + var $target = $(e.target); + var $editForm = $(this.getEditFormSelector($target)); + var $note = $target.closest('.note'); + var $currentlyEditing = $('.note.is-editting:visible'); + + if ($currentlyEditing.length) { + var isEditAllowed = this.checkContentToAllowEditing($currentlyEditing); + + if (!isEditAllowed) { + return; + } } - }; + $note.find('.js-note-attachment-delete').show(); + $editForm.addClass('current-note-edit-form'); + $note.addClass('is-editting'); + this.putEditFormInPlace($target); + }; /* Called in response to clicking the edit note link @@ -532,22 +546,43 @@ */ Notes.prototype.cancelEdit = function(e) { - var note; e.preventDefault(); - note = $(e.target).closest('.note'); + var $target = $(e.target); + var note = $target.closest('.note'); + note.find('.js-edit-warning').hide(); + this.revertNoteEditForm($target); return this.removeNoteEditForm(note); }; + Notes.prototype.revertNoteEditForm = function($target) { + $target = $target || $('.note.is-editting:visible'); + var selector = this.getEditFormSelector($target); + var $editForm = $(selector); + + $editForm.insertBefore('.notes-form'); + $editForm.find('.js-comment-button').enable(); + $editForm.find('.js-edit-warning').hide(); + }; + + Notes.prototype.getEditFormSelector = function($el) { + var selector = '.note-edit-form:not(.mr-note-edit-form)'; + + if ($el.parents('#diffs').length) { + selector = '.note-edit-form.mr-note-edit-form'; + } + + return selector; + }; + Notes.prototype.removeNoteEditForm = function(note) { - var form; - form = note.find(".current-note-edit-form"); - note.removeClass("is-editting"); - form.removeClass("current-note-edit-form"); + var form = note.find('.current-note-edit-form'); + note.removeClass('is-editting'); + form.removeClass('current-note-edit-form'); + form.find('.js-edit-warning').hide(); // Replace markdown textarea text with original note text. - return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note')); + return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note')); }; - /* Called in response to deleting a note of any kind. @@ -587,7 +622,6 @@ return this.updateNotesCount(-1); }; - /* Called in response to clicking the delete attachment link @@ -604,7 +638,6 @@ return note.find(".current-note-edit-form").remove(); }; - /* Called when clicking on the "reply" button for a diff line. @@ -624,7 +657,6 @@ return this.setupDiscussionNoteForm(replyLink, form); }; - /* Shows the diff or discussion form and does some setup on it. @@ -666,7 +698,6 @@ .addClass("discussion-form js-discussion-note-form"); }; - /* Called when clicking on the "add a comment" button on the side of a diff line. @@ -723,7 +754,6 @@ } }; - /* Called in response to "cancel" on a diff note form. @@ -757,7 +787,6 @@ return this.removeDiscussionNoteForm(form); }; - /* Called after an attachment file has been selected. @@ -772,7 +801,6 @@ return form.find(".js-attachment-filename").text(filename); }; - /* Called when the tab visibility changes */ @@ -837,19 +865,50 @@ Notes.prototype.initTaskList = function() { this.enableTaskList(); - return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList); + return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList.bind(this)); }; Notes.prototype.enableTaskList = function() { return $('.note .js-task-list-container').taskList('enable'); }; - Notes.prototype.updateTaskList = function() { - return $('form', this).submit(); + Notes.prototype.putEditFormInPlace = function($el) { + var $editForm = $(this.getEditFormSelector($el)); + var $note = $el.closest('.note'); + + $editForm.insertAfter($note.find('.note-text')); + + var $originalContentEl = $note.find('.original-note-content'); + var originalContent = $originalContentEl.text().trim(); + var postUrl = $originalContentEl.data('post-url'); + var targetId = $originalContentEl.data('target-id'); + var targetType = $originalContentEl.data('target-type'); + + new GLForm($editForm.find('form')); + + $editForm.find('form') + .attr('action', postUrl) + .attr('data-remote', 'true'); + $editForm.find('.js-form-target-id').val(targetId); + $editForm.find('.js-form-target-type').val(targetType); + $editForm.find('.js-note-text').focus().val(originalContent); + $editForm.find('.js-md-write-button').trigger('click'); + $editForm.find('.referenced-users').hide(); + }; + + Notes.prototype.updateTaskList = function(e) { + var $target = $(e.target); + var $list = $target.closest('.js-task-list-container'); + var $editForm = $(this.getEditFormSelector($target)); + var $note = $list.closest('.note'); + + this.putEditFormInPlace($list); + $editForm.find('#note_note').val($note.find('.original-task-list').val()); + $('form', $list).submit(); }; Notes.prototype.updateNotesCount = function(updateCount) { - return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount); + return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); }; Notes.prototype.resolveDiscussion = function() { @@ -894,7 +953,5 @@ }; return Notes; - })(); - }).call(this); diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index 5d0d594073d..926dc35fee8 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */ /* global Flash */ (function() { @@ -27,7 +27,5 @@ } return NotificationsDropdown; - })(); - }).call(this); diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 2034f9a748a..c3d7cc0adfb 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -1,6 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, max-len */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.NotificationsForm = (function() { function NotificationsForm() { @@ -53,7 +53,5 @@ }; return NotificationsForm; - })(); - }).call(this); diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index 0b09ad113a3..43263368494 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -1,12 +1,10 @@ -/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, padded-blocks, no-param-reassign, max-len */ +/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */ //= require lib/utils/bootstrap_linked_tabs ((global) => { - class Pipelines { constructor(options = {}) { - if (options.initTabs && options.tabsOptions) { new global.LinkedTabs(options.tabsOptions); } @@ -37,5 +35,4 @@ } global.Pipelines = Pipelines; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 89f7e976934..07eea98e737 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -7,6 +7,7 @@ // (function () { var lastTextareaPreviewed; + var lastTextareaHeight = null; var markdownPreview; var previewButtonSelector; var writeButtonSelector; @@ -104,10 +105,14 @@ if (!$form) { return; } + lastTextareaPreviewed = $form.find('textarea.markdown-area'); + lastTextareaHeight = lastTextareaPreviewed.height(); + // toggle tabs $form.find(writeButtonSelector).parent().removeClass('active'); $form.find(previewButtonSelector).parent().addClass('active'); + // toggle content $form.find('.md-write-holder').hide(); $form.find('.md-preview-holder').show(); @@ -119,9 +124,15 @@ return; } lastTextareaPreviewed = null; + + if (lastTextareaHeight) { + $form.find('textarea.markdown-area').height(lastTextareaHeight); + } + // toggle tabs $form.find(writeButtonSelector).parent().addClass('active'); $form.find(previewButtonSelector).parent().removeClass('active'); + // toggle content $form.find('.md-write-holder').show(); $form.find('textarea.markdown-area').focus(); diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6 index b4b6da41f63..42e9847af91 100644 --- a/app/assets/javascripts/profile/gl_crop.js.es6 +++ b/app/assets/javascripts/profile/gl_crop.js.es6 @@ -1,14 +1,12 @@ -/* eslint-disable no-useless-escape, max-len, padded-blocks, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, no-plusplus, new-parens, semi */ +/* eslint-disable no-useless-escape, max-len, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, new-parens */ ((global) => { - // Matches everything but the file name const FILENAMEREGEX = /^.*[\\\/]/; class GitLabCrop { constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg, exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) { - this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this); this.onModalHide = this.onModalHide.bind(this); this.onModalShow = this.onModalShow.bind(this); @@ -136,7 +134,7 @@ var array, binary, i, k, len, v; binary = atob(dataURL.split(',')[1]); array = []; - for (k = i = 0, len = binary.length; i < len; k = ++i) { + for (k = i = 0, len = binary.length; i < len; k = (i += 1)) { v = binary[k]; array.push(binary.charCodeAt(k)); } @@ -169,6 +167,5 @@ return this.each(function() { return $(this).data('glcrop', new GitLabCrop(this, opts)); }); - } - + }; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6 index aef2e3a3fa8..5aec9c813fe 100644 --- a/app/assets/javascripts/profile/profile.js.es6 +++ b/app/assets/javascripts/profile/profile.js.es6 @@ -1,8 +1,7 @@ -/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len, padded-blocks */ +/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */ /* global Flash */ ((global) => { - class Profile { constructor({ form } = {}) { this.onSubmitForm = this.onSubmitForm.bind(this); @@ -37,6 +36,7 @@ } onSubmitForm(e) { + e.preventDefault(); return this.saveForm(); } @@ -95,5 +95,4 @@ return new Profile(); } }); - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index fcf3a4af956..7cf630a1d76 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,4 +1,4 @@ -/* 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, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */ +/* 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 Turbolinks */ /* global ProjectSelect */ @@ -94,11 +94,11 @@ return $el.text().trim(); }, clicked: function(selected, $el, e) { - e.preventDefault() + e.preventDefault(); if ($('input[name="ref"]').length) { - var $form = $dropdown.closest('form'), - action = $form.attr('action'), - divider = action.indexOf('?') < 0 ? '?' : '&'; + var $form = $dropdown.closest('form'); + var action = $form.attr('action'); + var divider = action.indexOf('?') < 0 ? '?' : '&'; Turbolinks.visit(action + '' + divider + '' + $form.serialize()); } } @@ -107,7 +107,5 @@ }; return Project; - })(); - }).call(this); diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js index 84f28ede4bf..a6d3ba9eb86 100644 --- a/app/assets/javascripts/project_avatar.js +++ b/app/assets/javascripts/project_avatar.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, no-useless-escape, max-len */ (function() { this.ProjectAvatar = (function() { function ProjectAvatar() { @@ -16,7 +16,5 @@ } return ProjectAvatar; - })(); - }).call(this); diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 1bd232314d0..04fe84683f3 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -1,8 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, prefer-template, no-unused-vars, no-return-assign */ /* global fuzzaldrinPlus */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.ProjectFindFile = (function() { var highlighter; @@ -71,7 +71,7 @@ var blobItemUrl, filePath, html, i, j, len, matches, results; this.element.find(".tree-table > tbody").empty(); results = []; - for (i = j = 0, len = filePaths.length; j < len; i = ++j) { + for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) { filePath = filePaths[i]; if (i === 20) { break; @@ -92,7 +92,7 @@ lastIndex = 0; highlightText = ""; matchedChars = []; - for (j = 0, len = matches.length; j < len; j++) { + for (j = 0, len = matches.length; j < len; j += 1) { matchIndex = matches[j]; unmatched = text.substring(lastIndex, matchIndex); if (unmatched) { @@ -167,7 +167,5 @@ }; return ProjectFindFile; - })(); - }).call(this); diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js index 4aedc9a2330..208f25a0e33 100644 --- a/app/assets/javascripts/project_fork.js +++ b/app/assets/javascripts/project_fork.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ (function() { this.ProjectFork = (function() { function ProjectFork() { @@ -9,7 +9,5 @@ } return ProjectFork; - })(); - }).call(this); diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index 02dafcfb865..6614d8952cd 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ /* global Turbolinks */ (function() { @@ -10,7 +10,5 @@ } return ProjectImport; - })(); - }).call(this); diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6 index b8d6a198996..8365f7118d5 100644 --- a/app/assets/javascripts/project_label_subscription.js.es6 +++ b/app/assets/javascripts/project_label_subscription.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, prefer-const, max-len, no-param-reassign, padded-blocks */ +/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, max-len, no-param-reassign */ (function(global) { class ProjectLabelSubscription { @@ -38,8 +38,8 @@ this.$buttons.attr('data-status', newStatus); this.$buttons.find('> span').text(newAction); - for (let button of this.$buttons) { - let $button = $(button); + for (const button of this.$buttons) { + const $button = $(button); if ($button.attr('data-original-title')) { $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); @@ -50,5 +50,4 @@ } global.ProjectLabelSubscription = ProjectLabelSubscription; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js index 7fc611d0dad..3aa6f6771ce 100644 --- a/app/assets/javascripts/project_new.js +++ b/app/assets/javascripts/project_new.js @@ -1,6 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, one-var, indent, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, radix, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */ + (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.ProjectNew = (function() { function ProjectNew() { @@ -14,18 +15,29 @@ return $('.save-project-loader').show(); }; })(this)); + + this.initVisibilitySelect(); + this.toggleSettings(); this.toggleSettingsOnclick(); this.toggleRepoVisibility(); } + ProjectNew.prototype.initVisibilitySelect = function() { + const visibilityContainer = document.querySelector('.js-visibility-select'); + if (!visibilityContainer) return; + const visibilitySelect = new gl.VisibilitySelect(visibilityContainer); + visibilitySelect.init(); + }; + ProjectNew.prototype.toggleSettings = function() { var self = this; this.$selects.each(function () { - var $select = $(this), - className = $select.data('field').replace(/_/g, '-') - .replace('access-level', 'feature'); + var $select = $(this); + var className = $select.data('field') + .replace(/_/g, '-') + .replace('access-level', 'feature'); self._showOrHide($select, '.' + className); }); }; @@ -45,9 +57,9 @@ }; ProjectNew.prototype.toggleRepoVisibility = function () { - var $repoAccessLevel = $('.js-repo-access-level select'), - containerRegistry = document.querySelectorAll('.js-container-registry')[0], - containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); + var $repoAccessLevel = $('.js-repo-access-level select'); + var containerRegistry = document.querySelectorAll('.js-container-registry')[0]; + var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled'); this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']") .nextAll() @@ -55,11 +67,11 @@ $repoAccessLevel.off('change') .on('change', function () { - var selectedVal = parseInt($repoAccessLevel.val()); + var selectedVal = parseInt($repoAccessLevel.val(), 10); this.$repoSelects.each(function () { - var $this = $(this), - repoSelectVal = parseInt($this.val()); + var $this = $(this); + var repoSelectVal = parseInt($this.val(), 10); $this.find('option').show(); @@ -88,7 +100,5 @@ }; return ProjectNew; - })(); - }).call(this); diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 38bc2e1c3a0..7b5e9953598 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */ /* global Api */ (function() { @@ -100,7 +100,5 @@ } return ProjectSelect; - })(); - }).call(this); diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js index eaf4c03d573..aad130cf267 100644 --- a/app/assets/javascripts/project_show.js +++ b/app/assets/javascripts/project_show.js @@ -1,12 +1,11 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife */ + (function() { this.ProjectShow = (function() { function ProjectShow() {} return ProjectShow; - })(); - }).call(this); // I kept class for future diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js index 4548dc68fe1..69a11dfaf39 100644 --- a/app/assets/javascripts/projects_list.js +++ b/app/assets/javascripts/projects_list.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, max-len */ (function() { window.ProjectsList = { @@ -47,5 +47,4 @@ }); } }; - }).call(this); diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 index 4aef1c84b56..e7fff57ff45 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 @@ -1,7 +1,7 @@ -/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, object-shorthand, no-else-return, comma-dangle, semi, padded-blocks, max-len */ +/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, max-len */ (global => { - global.gl = global.gl || {}; + global.gl = global.gl || {}; gl.ProtectedBranchAccessDropdown = class { constructor(options) { @@ -25,6 +25,5 @@ } }); } - } - + }; })(window); diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 index f26fba979a4..57ea2f52814 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 @@ -1,8 +1,8 @@ -/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, comma-dangle, padded-blocks, semi, max-len */ +/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */ /* global ProtectedBranchDropdown */ (global => { - global.gl = global.gl || {}; + global.gl = global.gl || {}; gl.ProtectedBranchCreate = class { constructor() { @@ -44,7 +44,6 @@ // This will run after clicked callback onSelect() { - // Enable submit button const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]'); @@ -52,6 +51,5 @@ this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length)); } - } - + }; })(window); diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 index 4ff2fa5a80f..149e511451e 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 @@ -1,8 +1,8 @@ -/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, padded-blocks, comma-dangle, no-trailing-spaces, semi, max-len */ +/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */ /* global Flash */ (global => { - global.gl = global.gl || {}; + global.gl = global.gl || {}; gl.ProtectedBranchEdit = class { constructor(options) { @@ -14,7 +14,6 @@ } buildDropdowns() { - // Allowed to merge dropdown new gl.ProtectedBranchAccessDropdown({ $dropdown: this.$allowedToMergeDropdown, @@ -63,6 +62,5 @@ } }); } - } - + }; })(window); diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 index b6972ef2e16..336fa6c57a7 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 @@ -1,10 +1,10 @@ -/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, no-new, comma-dangle, semi, padded-blocks, max-len */ +/* eslint-disable arrow-parens, no-param-reassign, no-new, comma-dangle */ (global => { - global.gl = global.gl || {}; + global.gl = global.gl || {}; gl.ProtectedBranchEditList = class { - constructor() { + constructor() { this.$wrap = $('.protected-branches-list'); // Build edit forms @@ -14,6 +14,5 @@ }); }); } - } - + }; })(window); diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index bbb2f186655..0caf8ba4344 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len */ // Render Gitlab flavoured Markdown // // Delegates to syntax highlight and render math @@ -12,5 +12,4 @@ $(document).on('ready page:load', function() { return $('body').renderGFM(); }); - }).call(this); diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js index 209e7a8661d..6cef449babf 100644 --- a/app/assets/javascripts/render_math.js +++ b/app/assets/javascripts/render_math.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len, no-console */ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, max-len, no-console */ // Renders math using KaTeX in any element with the // `js-render-math` class // @@ -51,5 +51,4 @@ }); } }; - }).call(this); diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index b1e844b7302..76a0f993ea0 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -1,8 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */ +/* 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 */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.Sidebar = (function() { function Sidebar(currentUser) { @@ -18,7 +18,7 @@ $('.dropdown').off('loading.gl.dropdown'); $('.dropdown').off('loaded.gl.dropdown'); $(document).off('click', '.js-sidebar-toggle'); - } + }; Sidebar.prototype.addEventListeners = function() { this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); @@ -200,7 +200,5 @@ }; return Sidebar; - })(); - }).call(this); diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 4b6ebadeac7..489e567259c 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */ /* global Api */ (function() { @@ -12,6 +12,9 @@ selectable: true, filterable: true, fieldName: 'group_id', + search: { + fields: ['name'] + }, data: function(term, callback) { return Api.groups(term, {}, function(data) { data.unshift({ @@ -40,6 +43,9 @@ selectable: true, filterable: true, fieldName: 'project_id', + search: { + fields: ['name'] + }, data: function(term, callback) { return Api.projects(term, 'id', function(data) { data.unshift({ @@ -90,7 +96,5 @@ }; return Search; - })(); - }).call(this); diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6 index 437f5dbbf7d..6250e75d407 100644 --- a/app/assets/javascripts/search_autocomplete.js.es6 +++ b/app/assets/javascripts/search_autocomplete.js.es6 @@ -1,7 +1,6 @@ -/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, no-plusplus, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, padded-blocks, no-extra-semi, indent, max-len */ +/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */ ((global) => { - const KEYCODE = { ESCAPE: 27, BACKSPACE: 8, @@ -70,12 +69,17 @@ search: { fields: ['text'] }, + id: this.getSearchText, data: this.getData.bind(this), selectable: true, clicked: this.onClick.bind(this) }); } + getSearchText(selectedObject, el) { + return selectedObject.id ? selectedObject.text : ''; + } + getData(term, callback) { var _this, contents, jqXHR; _this = this; @@ -105,7 +109,7 @@ data = []; // List results firstCategory = true; - for (i = 0, len = response.length; i < len; i++) { + for (i = 0, len = response.length; i < len; i += 1) { suggestion = response[i]; // Add group header before list each group if (lastCategory !== suggestion.category) { @@ -142,8 +146,9 @@ } getCategoryContents() { - var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; + var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils; userId = gon.current_user_id; + userName = gon.current_username; utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; if (utils.isInGroupsPage() && groupOptions) { options = groupOptions[utils.getGroupSlug()]; @@ -158,10 +163,10 @@ header: "" + name }, { text: 'Issues assigned to me', - url: issuesPath + "/?assignee_id=" + userId + url: issuesPath + "/?assignee_username=" + userName }, { text: "Issues I've created", - url: issuesPath + "/?author_id=" + userId + url: issuesPath + "/?author_username=" + userName }, 'separator', { text: 'Merge requests assigned to me', url: mrPath + "/?assignee_id=" + userId @@ -215,7 +220,7 @@ this.dropdown.addClass('open').trigger('shown.bs.dropdown'); return this.searchInput.removeClass('disabled'); } - }; + } // Saves last length of the entered text onSearchInputKeyDown() { @@ -279,12 +284,12 @@ return this.searchInput.val(); } - onClearInputClick(e) { + onClearInputClick(e) { e.preventDefault(); return this.searchInput.val('').focus(); } - onSearchInputBlur(e) { + onSearchInputBlur(e) { this.isFocused = false; this.wrap.removeClass('search-active'); // If input is blank then restore state @@ -304,12 +309,12 @@ hasLocationBadge() { return this.wrap.is('.has-location-badge'); - }; + } restoreOriginalState() { var i, input, inputs, len; inputs = Object.keys(this.originalState); - for (i = 0, len = inputs.length; i < len; i++) { + for (i = 0, len = inputs.length; i < len; i += 1) { input = inputs[i]; this.getElement("#" + input).val(this.originalState[input]); } @@ -330,7 +335,7 @@ var i, input, inputs, len, results; inputs = Object.keys(this.originalState); results = []; - for (i = 0, len = inputs.length; i < len; i++) { + for (i = 0, len = inputs.length; i < len; i += 1) { input = inputs[i]; // _location isnt a input if (input === '_location') { @@ -360,11 +365,11 @@ var html; html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>"; return this.dropdownContent.html(html); - }; + } onClick(item, $el, e) { if (location.pathname.indexOf(item.url) !== -1) { - e.preventDefault(); + if (!e.metaKey) e.preventDefault(); if (!this.badgePresent) { if (item.category === 'Projects') { this.projectInputEl.val(item.id); @@ -383,8 +388,7 @@ this.disableAutocomplete(); return this.searchInput.val('').focus(); } - }; - + } } global.SearchAutocomplete = SearchAutocomplete; @@ -425,5 +429,4 @@ }; } }); - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index 5ea00f408f4..c56ee429b8e 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,10 +1,10 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ /* global Mousetrap */ /* global Turbolinks */ /* global findFileURL */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.Shortcuts = (function() { function Shortcuts(skipResetBindings) { @@ -51,7 +51,7 @@ var i, l, len, results; if (location && location.length > 0) { results = []; - for (i = 0, len = location.length; i < len; i++) { + for (i = 0, len = location.length; i < len; i += 1) { l = location[i]; results.push($(l).show()); } @@ -78,7 +78,6 @@ }; return Shortcuts; - })(); $(document).on('click.more_help', '.js-more-help-button', function(e) { @@ -99,5 +98,4 @@ } }; })(); - }).call(this); diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index c26903038b4..d50ddd98de1 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,4 +1,4 @@ -/* 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, consistent-return, padded-blocks */ +/* 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, consistent-return */ /* global Shortcuts */ /* global Mousetrap */ @@ -25,7 +25,5 @@ }; return ShortcutsBlob; - })(Shortcuts); - }).call(this); diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 4549742bbcb..603fefbf15a 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -1,4 +1,4 @@ -/* 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-arrow-callback, consistent-return, no-return-assign, padded-blocks */ +/* 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-arrow-callback, consistent-return, no-return-assign */ /* global Mousetrap */ /* global Shortcuts */ @@ -36,7 +36,5 @@ }; return ShortcutsDashboardNavigation; - })(Shortcuts); - }).call(this); diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 3a81380eef0..8469837533b 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -1,4 +1,4 @@ -/* 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, padded-blocks */ +/* 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 */ /* global Mousetrap */ /* global ShortcutsNavigation */ @@ -34,7 +34,5 @@ } return ShortcutsFindFile; - })(ShortcutsNavigation); - }).call(this); diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index b892fbc3393..4ef516af8c8 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */ /* global Mousetrap */ /* global Turbolinks */ /* global ShortcutsNavigation */ @@ -39,29 +39,39 @@ } ShortcutsIssuable.prototype.replyWithSelectedText = function() { - var quote, replyField, selected, separator; - if (window.getSelection) { - selected = window.getSelection().toString(); - replyField = $('.js-main-target-form #note_note'); - if (selected.trim() === "") { - return; - } - // Put a '>' character before each non-empty line in the selection - quote = _.map(selected.split("\n"), function(val) { - if (val.trim() !== '') { - return "> " + val + "\n"; - } - }); - // If replyField already has some content, add a newline before our quote - separator = replyField.val().trim() !== "" && "\n" || ''; - replyField.val(function(_, current) { - return current + separator + quote.join('') + "\n"; - }); - // Trigger autosave for the added text - replyField.trigger('input'); - // Focus the input field - return replyField.focus(); + var quote, replyField, documentFragment, selected, separator; + + documentFragment = window.gl.utils.getSelectedFragment(); + if (!documentFragment) return; + + // If the documentFragment contains more than just Markdown, don't copy as GFM. + if (documentFragment.querySelector('.md, .wiki')) return; + + selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment); + + replyField = $('.js-main-target-form #note_note'); + if (selected.trim() === "") { + return; } + quote = _.map(selected.split("\n"), function(val) { + return ("> " + val).trim() + "\n"; + }); + // If replyField already has some content, add a newline before our quote + separator = replyField.val().trim() !== "" && "\n\n" || ''; + replyField.val(function(_, current) { + return current + separator + quote.join('') + "\n"; + }); + + // Trigger autosave + replyField.trigger('input'); + + // Trigger autosize + var event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + replyField.get(0).dispatchEvent(event); + + // Focus the input field + return replyField.focus(); }; ShortcutsIssuable.prototype.editIssue = function() { @@ -76,7 +86,5 @@ }; return ShortcutsIssuable; - })(ShortcutsNavigation); - }).call(this); diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 0776d0a9b67..afeda0dd5fe 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */ /* global Mousetrap */ /* global Shortcuts */ @@ -64,7 +64,5 @@ }; return ShortcutsNavigation; - })(Shortcuts); - }).call(this); diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index ecc3fab84c3..79896e35cbb 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, max-len */ /* global Mousetrap */ /* global ShortcutsNavigation */ @@ -24,7 +24,5 @@ } return ShortcutsNetwork; - })(ShortcutsNavigation); - }).call(this); diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index 9790a44972d..05234643c18 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign, padded-blocks */ +/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */ /* global Cookies */ ((global) => { @@ -94,5 +94,4 @@ } global.Sidebar = Sidebar; - })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index ac8603ccd10..5b20c63384c 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -1,7 +1,7 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER; @@ -14,8 +14,7 @@ COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; - function SingleFileDiff(file, forceLoad, cb) { - var clickTarget; + function SingleFileDiff(file) { this.file = file; this.toggleDiff = bind(this.toggleDiff, this); this.content = $('.diff-content', this.file); @@ -33,14 +32,13 @@ this.content.after(this.collapsedContent); this.$toggleIcon.addClass('fa-caret-down'); } - clickTarget = $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff); - if (forceLoad) { - this.toggleDiff({ target: clickTarget }, cb); - } + + $('.file-title, .click-to-expand', this.file).on('click', (function (e) { + this.toggleDiff($(e.target)); + }).bind(this)); } - SingleFileDiff.prototype.toggleDiff = function(e, cb) { - var $target = $(e.target); + SingleFileDiff.prototype.toggleDiff = function($target, cb) { if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; this.isOpen = !this.isOpen; if (!this.isOpen && !this.hasError) { @@ -88,15 +86,13 @@ }; return SingleFileDiff; - })(); - $.fn.singleFileDiff = function(forceLoad, cb) { + $.fn.singleFileDiff = function() { return this.each(function() { - if (!$.data(this, 'singleFileDiff') || forceLoad) { - return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this, forceLoad, cb)); + if (!$.data(this, 'singleFileDiff')) { + return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this)); } }); }; - }).call(this); diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 18512d179b3..cfb4ff82a73 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,15 +1,14 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, semi, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */ /* global ace */ /*= require_tree . */ (function() { $(function() { - var editor = ace.edit("editor") + var editor = ace.edit("editor"); $(".snippet-form-holder form").on('submit', function() { $(".snippet-file-content").val(editor.getValue()); }); }); - }).call(this); diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6 index 6f913326a3a..2128007113f 100644 --- a/app/assets/javascripts/snippets_list.js.es6 +++ b/app/assets/javascripts/snippets_list.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, semi, max-len */ +/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */ (global => { global.gl = global.gl || {}; @@ -9,5 +9,5 @@ $holder.find('.pagination').on('ajax:success', (e, data) => { $holder.replaceWith(data.html); }); - } + }; })(window); diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js index f1fc526bf2e..531fd0e9c32 100644 --- a/app/assets/javascripts/star.js +++ b/app/assets/javascripts/star.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ /* global Flash */ (function() { @@ -26,7 +26,5 @@ } return Star; - })(); - }).call(this); diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index 932120157a3..d8191605128 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -1,6 +1,3 @@ -//= require vue -//= require vue-resource - (() => { /* * SubbableResource can be extended to provide a pubsub-style service for one-off REST diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js index 185d20775d0..187356f0bf9 100644 --- a/app/assets/javascripts/subscription_select.js +++ b/app/assets/javascripts/subscription_select.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ (function() { this.SubscriptionSelect = (function() { function SubscriptionSelect() { @@ -30,7 +30,5 @@ } return SubscriptionSelect; - })(); - }).call(this); diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index 5d0fa62c50a..115716bff6a 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, max-len */ // Syntax Highlighter // @@ -10,7 +10,6 @@ // <div class="js-syntax-highlight"></div> // (function() { - $.fn.syntaxHighlight = function() { var $children; @@ -25,5 +24,4 @@ } } }; - }).call(this); diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index d2b152045b4..b0132af70f2 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable prefer-const, comma-dangle, max-len, no-useless-return, object-curly-spacing, no-param-reassign, max-len */ +/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ /* global Api */ /*= require ../blob/template_selector */ @@ -12,7 +12,7 @@ this.issuableType = this.wrapper.data('issuable-type'); this.titleInput = $(`#${this.issuableType}_title`); - let initialQuery = { + const initialQuery = { name: this.dropdown.data('selected') }; @@ -23,7 +23,7 @@ }); $('.no-template', this.dropdown.parent()).on('click', () => { - this.currentTemplate = ''; + this.currentTemplate.content = ''; this.setInputValueToTemplateContent(); $('.dropdown-toggle-text', this.dropdown).text('Choose a template'); }); @@ -47,10 +47,10 @@ // 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}); + this.requestFileSuccess(this.currentTemplate, { skipFocus: true }); this.titleInput.focus(); } else { - this.requestFileSuccess(this.currentTemplate, {skipFocus: false}); + this.requestFileSuccess(this.currentTemplate, { skipFocus: false }); } return; } diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 index 7310b9de074..97f6d37364d 100644 --- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-new, comma-dangle, class-methods-use-this, prefer-const, no-param-reassign */ +/* eslint-disable no-new, comma-dangle, class-methods-use-this, no-param-reassign */ ((global) => { class IssuableTemplateSelectors { @@ -19,7 +19,7 @@ } initEditor() { - let editor = $('.markdown-area'); + const editor = $('.markdown-area'); // Proxy ace-editor's .setValue to jQuery's .val editor.setValue = editor.val; editor.getValue = editor.val; diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6 index ded7ee6e9fe..33d2c1e1a17 100644 --- a/app/assets/javascripts/terminal/terminal_bundle.js.es6 +++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6 @@ -1,3 +1,5 @@ +//= require xterm/encoding-indexes +//= require xterm/encoding //= require xterm/xterm.js //= require xterm/fit.js //= require ./terminal.js diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index d8713600030..ef9c0a885fb 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,9 +1,8 @@ -/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len */ +/* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */ /* global UsersSelect */ /* global Turbolinks */ ((global) => { - class Todos { constructor({ el } = {}) { this.allDoneClicked = this.allDoneClicked.bind(this); @@ -49,7 +48,7 @@ clicked: function() { return $dropdown.closest('form.filter-form').submit(); } - }) + }); } doneClicked(e) { diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index f48a7ee0f55..d124ca4f88b 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */ /* global Turbolinks */ (function() { this.TreeView = (function() { @@ -64,7 +64,5 @@ }; return TreeView; - })(); - }).call(this); diff --git a/app/assets/javascripts/u2f/authenticate.js.es6 b/app/assets/javascripts/u2f/authenticate.js.es6 index 2b992109a8c..500b78fc5d8 100644 --- a/app/assets/javascripts/u2f/authenticate.js.es6 +++ b/app/assets/javascripts/u2f/authenticate.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */ /* global u2f */ /* global U2FError */ /* global U2FUtil */ @@ -10,7 +10,7 @@ (function() { const global = window.gl || (window.gl = {}); - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; global.U2FAuthenticate = (function() { function U2FAuthenticate(container, form, u2fParams, fallbackButton, fallbackUI) { @@ -57,7 +57,7 @@ return function(response) { var error; if (response.errorCode) { - error = new U2FError(response.errorCode); + error = new U2FError(response.errorCode, 'authenticate'); return _this.renderError(error); } else { return _this.renderAuthenticated(JSON.stringify(response)); @@ -114,7 +114,5 @@ }; return U2FAuthenticate; - })(); - })(); diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index bb9942a3aa0..86b459e1866 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,29 +1,27 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, max-len */ /* global u2f */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.U2FError = (function() { - function U2FError(errorCode) { + function U2FError(errorCode, u2fFlowType) { this.errorCode = errorCode; this.message = bind(this.message, this); this.httpsDisabled = window.location.protocol !== 'https:'; + this.u2fFlowType = u2fFlowType; } U2FError.prototype.message = function() { - switch (false) { - case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled): - return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."; - case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE: - return "This device has already been registered with us."; - default: - return "There was a problem communicating with your device."; + if (this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled) { + return 'U2F only works with HTTPS-enabled websites. Contact your administrator for more details.'; + } else if (this.errorCode === u2f.ErrorCodes.DEVICE_INELIGIBLE) { + if (this.u2fFlowType === 'authenticate') return 'This device has not been registered with us.'; + if (this.u2fFlowType === 'register') return 'This device has already been registered with us.'; } + return "There was a problem communicating with your device."; }; return U2FError; - })(); - }).call(this); diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 050c9bfc02e..69d1ff3a39e 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, max-len */ /* global u2f */ /* global U2FError */ /* global U2FUtil */ @@ -8,7 +8,7 @@ // State Flow #1: setup -> in_progress -> registered -> POST to server // State Flow #2: setup -> in_progress -> error -> setup (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.U2FRegister = (function() { function U2FRegister(container, u2fParams) { @@ -39,7 +39,7 @@ return function(response) { var error; if (response.errorCode) { - error = new U2FError(response.errorCode); + error = new U2FError(response.errorCode, 'register'); return _this.renderError(error); } else { return _this.renderRegistered(JSON.stringify(response)); @@ -94,7 +94,5 @@ }; return U2FRegister; - })(); - }).call(this); diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js index eedd3bcd5a1..34e88220b12 100644 --- a/app/assets/javascripts/u2f/util.js +++ b/app/assets/javascripts/u2f/util.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife */ (function() { this.U2FUtil = (function() { function U2FUtil() {} @@ -8,7 +8,5 @@ }; return U2FUtil; - })(); - }).call(this); diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6 index 0a2db7c05fe..059e6c628b3 100644 --- a/app/assets/javascripts/user.js.es6 +++ b/app/assets/javascripts/user.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign, semi */ +/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign */ /* global Cookies */ ((global) => { @@ -30,5 +30,5 @@ $(this).parents('.project-limit-message').remove(); }); } - } + }; })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 index b9c23b51b4d..313fb17aee8 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, array-bracket-spacing, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, semi, 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 */ /* UserTabs @@ -107,7 +107,7 @@ content on the Users#show page. this.loadActivities(source); } - const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ]; + const loadableActions = ['groups', 'contributed', 'projects', 'snippets']; if (loadableActions.indexOf(action) > -1) { return this.loadTab(source, action); } @@ -145,7 +145,7 @@ content on the Users#show page. } setCurrentAction(source, action) { - let new_state = source + let new_state = source; new_state = new_state.replace(/\/+$/, ''); new_state += this._location.search + this._location.hash; history.replaceState({ diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 578be7c3590..e7280d643d3 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,9 +1,9 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */ /* global d3 */ /* global dateFormat */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; this.Calendar = (function() { function Calendar(timestamps, calendar_activities_path) { @@ -20,7 +20,7 @@ this.timestampsTmp = []; var group = 0; - var today = new Date() + var today = new Date(); today.setHours(0, 0, 0, 0, 0); var oneYearAgo = new Date(today); @@ -28,7 +28,7 @@ var days = gl.utils.getDayDifference(oneYearAgo, today); - for(var i = 0; i <= days; i++) { + for (var i = 0; i <= days; i += 1) { var date = new Date(oneYearAgo); date.setDate(date.getDate() + i); @@ -39,7 +39,7 @@ // or if is first object if ((day === 0 && i !== 0) || i === 0) { this.timestampsTmp.push([]); - group++; + group += 1; } var innerArray = this.timestampsTmp[group - 1]; @@ -74,7 +74,7 @@ } return extraWidthPadding; - } + }; Calendar.prototype.renderSvg = function(group) { var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group); @@ -158,7 +158,7 @@ }; Calendar.prototype.renderMonths = function() { - return this.svg.append('g').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) { + return this.svg.append('g').attr('direction', 'ltr').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) { return date.x; }).attr('y', 10).attr('class', 'user-contrib-text').text((function(_this) { return function(date) { @@ -221,7 +221,5 @@ }; return Calendar; - })(); - }).call(this); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index d4b5e03aa35..77d2764cdf0 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -1,10 +1,10 @@ -/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, 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-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks */ +/* 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 */ (function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, slice = [].slice; this.UsersSelect = (function() { @@ -116,7 +116,7 @@ showDivider = 0; if (firstUser) { // Move current user to the front of the list - for (index = j = 0, len = users.length; j < len; index = ++j) { + for (index = j = 0, len = users.length; j < len; index = (j += 1)) { obj = users[index]; if (obj.username === firstUser) { users.splice(index, 1); @@ -278,7 +278,7 @@ if (firstUser) { // Move current user to the front of the list ref = data.results; - for (index = j = 0, len = ref.length; j < len; index = ++j) { + for (index = j = 0, len = ref.length; j < len; index = (j += 1)) { obj = ref[index]; if (obj.username === firstUser) { data.results.splice(index, 1); @@ -371,7 +371,7 @@ }; UsersSelect.prototype.user = function(user_id, callback) { - if(!/^\d+$/.test(user_id)) { + if (!/^\d+$/.test(user_id)) { return false; } @@ -421,7 +421,5 @@ }; return UsersSelect; - })(); - }).call(this); diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js.es6 new file mode 100644 index 00000000000..1fa2b5ac399 --- /dev/null +++ b/app/assets/javascripts/version_check_image.js.es6 @@ -0,0 +1,10 @@ +(() => { + class VersionCheckImage { + static bindErrorEvent(imageElement) { + imageElement.off('error').on('error', () => imageElement.hide()); + } + } + + window.gl = window.gl || {}; + gl.VersionCheckImage = VersionCheckImage; +})(); diff --git a/app/assets/javascripts/visibility_select.js.es6 b/app/assets/javascripts/visibility_select.js.es6 new file mode 100644 index 00000000000..f712d7ba930 --- /dev/null +++ b/app/assets/javascripts/visibility_select.js.es6 @@ -0,0 +1,27 @@ +(() => { + const gl = window.gl || (window.gl = {}); + + class VisibilitySelect { + constructor(container) { + if (!container) throw new Error('VisibilitySelect requires a container element as argument 1'); + this.container = container; + this.helpBlock = this.container.querySelector('.help-block'); + this.select = this.container.querySelector('select'); + } + + init() { + if (this.select) { + this.updateHelpText(); + this.select.addEventListener('change', this.updateHelpText.bind(this)); + } else { + this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock; + } + } + + updateHelpText() { + this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description; + } + } + + gl.VisibilitySelect = VisibilitySelect; +})(); diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_pagination/index.js.es6 new file mode 100644 index 00000000000..605824fa939 --- /dev/null +++ b/app/assets/javascripts/vue_pagination/index.js.es6 @@ -0,0 +1,148 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign, no-plusplus */ + +((gl) => { + const PAGINATION_UI_BUTTON_LIMIT = 4; + const UI_LIMIT = 6; + const SPREAD = '...'; + const PREV = 'Prev'; + const NEXT = 'Next'; + const FIRST = '<< First'; + const LAST = 'Last >>'; + + gl.VueGlPagination = Vue.extend({ + props: { + + /** + This function will take the information given by the pagination component + And make a new Turbolinks call + + Here is an example `change` method: + + change(pagenum, apiScope) { + Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + }, + */ + + change: { + type: Function, + required: true, + }, + + /** + pageInfo will come from the headers of the API call + in the `.then` clause of the VueResource API call + there should be a function that contructs the pageInfo for this component + + This is an example: + + const pageInfo = headers => ({ + perPage: +headers['X-Per-Page'], + page: +headers['X-Page'], + total: +headers['X-Total'], + totalPages: +headers['X-Total-Pages'], + nextPage: +headers['X-Next-Page'], + previousPage: +headers['X-Prev-Page'], + }); + */ + + pageInfo: { + type: Object, + required: true, + }, + }, + methods: { + changePage(e) { + let apiScope = gl.utils.getParameterByName('scope'); + + if (!apiScope) apiScope = 'all'; + + const text = e.target.innerText; + const { totalPages, nextPage, previousPage } = this.pageInfo; + + switch (text) { + case SPREAD: + break; + case LAST: + this.change(totalPages, apiScope); + break; + case NEXT: + this.change(nextPage, apiScope); + break; + case PREV: + this.change(previousPage, apiScope); + break; + case FIRST: + this.change(1, apiScope); + break; + default: + this.change(+text, apiScope); + break; + } + }, + }, + computed: { + prev() { + return this.pageInfo.previousPage; + }, + next() { + return this.pageInfo.nextPage; + }, + getItems() { + const total = this.pageInfo.totalPages; + const page = this.pageInfo.page; + const items = []; + + if (page > 1) items.push({ title: FIRST }); + + if (page > 1) { + items.push({ title: PREV, prev: true }); + } else { + items.push({ title: PREV, disabled: true, prev: true }); + } + + if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); + + const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); + const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); + + for (let i = start; i <= end; i++) { + const isActive = i === page; + items.push({ title: i, active: isActive, page: true }); + } + + if (total - page > PAGINATION_UI_BUTTON_LIMIT) { + items.push({ title: SPREAD, separator: true, page: true }); + } + + if (page === total) { + items.push({ title: NEXT, disabled: true, next: true }); + } else if (total - page >= 1) { + items.push({ title: NEXT, next: true }); + } + + if (total - page >= 1) items.push({ title: LAST, last: true }); + + return items; + }, + }, + template: ` + <div class="gl-pagination"> + <ul class="pagination clearfix"> + <li v-for='item in getItems' + :class='{ + page: item.page, + prev: item.prev, + next: item.next, + separator: item.separator, + active: item.active, + disabled: item.disabled + }' + > + <a @click="changePage($event)">{{item.title}}</a> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 new file mode 100644 index 00000000000..edd01f17a97 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -0,0 +1,42 @@ +/* global Vue, VueResource, gl */ +/*= require vue_common_component/commit */ +/*= require vue_pagination/index */ +/*= require vue-resource +/*= require boards/vue_resource_interceptor */ +/*= require ./status.js.es6 */ +/*= require ./store.js.es6 */ +/*= require ./pipeline_url.js.es6 */ +/*= require ./stage.js.es6 */ +/*= require ./stages.js.es6 */ +/*= require ./pipeline_actions.js.es6 */ +/*= require ./time_ago.js.es6 */ +/*= require ./pipelines.js.es6 */ + +(() => { + const project = document.querySelector('.pipelines'); + const entry = document.querySelector('.vue-pipelines-index'); + const svgs = document.querySelector('.pipeline-svgs'); + + Vue.use(VueResource); + + if (!entry) return null; + return new Vue({ + el: entry, + data: { + scope: project.dataset.url, + store: new gl.PipelineStore(), + svgs: svgs.dataset, + }, + components: { + 'vue-pipelines': gl.VuePipelines, + }, + template: ` + <vue-pipelines + :scope='scope' + :store='store' + :svgs='svgs' + > + </vue-pipelines> + `, + }); +})(); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 new file mode 100644 index 00000000000..b195b0ef3ba --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -0,0 +1,108 @@ +/* global Vue, Flash, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VuePipelineActions = Vue.extend({ + props: ['pipeline', 'svgs'], + computed: { + actions() { + return this.pipeline.details.manual_actions.length > 0; + }, + artifacts() { + return this.pipeline.details.artifacts.length > 0; + }, + }, + methods: { + download(name) { + return `Download ${name} artifacts`; + }, + }, + template: ` + <td class="pipeline-actions hidden-xs"> + <div class="controls pull-right"> + <div class="btn-group inline"> + <div class="btn-group"> + <button + v-if='actions' + class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" + data-toggle="dropdown" + title="Manual build" + data-placement="top" + data-toggle="dropdown" + aria-label="Manual build" + > + <span v-html='svgs.iconPlay' aria-hidden="true"></span> + <i class="fa fa-caret-down" aria-hidden="true"></i> + </button> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for='action in pipeline.details.manual_actions'> + <a + rel="nofollow" + data-method="post" + :href='action.path' + > + <span v-html='svgs.iconPlay' aria-hidden="true"></span> + <span>{{action.name}}</span> + </a> + </li> + </ul> + </div> + <div class="btn-group"> + <button + v-if='artifacts' + class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" + data-toggle="dropdown" + title="Artifacts" + data-placement="top" + data-toggle="dropdown" + aria-label="Artifacts" + > + <i class="fa fa-download" aria-hidden="true"></i> + <i class="fa fa-caret-down" aria-hidden="true"></i> + </button> + <ul class="dropdown-menu dropdown-menu-align-right"> + <li v-for='artifact in pipeline.details.artifacts'> + <a + rel="nofollow" + :href='artifact.path' + > + <i class="fa fa-download" aria-hidden="true"></i> + <span>{{download(artifact.name)}}</span> + </a> + </li> + </ul> + </div> + </div> + <div class="cancel-retry-btns inline"> + <a + v-if='pipeline.flags.retryable' + class="btn has-tooltip" + title="Retry" + rel="nofollow" + data-method="post" + data-placement="top" + data-toggle="dropdown" + :href='pipeline.retry_path' + aria-label="Retry" + > + <i class="fa fa-repeat" aria-hidden="true"></i> + </a> + <a + v-if='pipeline.flags.cancelable' + class="btn btn-remove has-tooltip" + title="Cancel" + rel="nofollow" + data-method="post" + data-placement="top" + data-toggle="dropdown" + :href='pipeline.cancel_path' + aria-label="Cancel" + > + <i class="fa fa-remove" aria-hidden="true"></i> + </a> + </div> + </div> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 new file mode 100644 index 00000000000..ae5649f0519 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js.es6 @@ -0,0 +1,63 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VuePipelineUrl = Vue.extend({ + props: [ + 'pipeline', + ], + computed: { + user() { + return !!this.pipeline.user; + }, + }, + template: ` + <td> + <a :href='pipeline.path'> + <span class="pipeline-id">#{{pipeline.id}}</span> + </a> + <span>by</span> + <a + v-if='user' + :href='pipeline.user.web_url' + > + <img + v-if='user' + class="avatar has-tooltip s20 " + :title='pipeline.user.name' + data-container="body" + :src='pipeline.user.avatar_url' + > + </a> + <span + v-if='!user' + class="api monospace" + > + API + </span> + <span + v-if='pipeline.flags.latest' + class="label label-success has-tooltip" + title="Latest pipeline for this branch" + data-original-title="Latest pipeline for this branch" + > + latest + </span> + <span + v-if='pipeline.flags.yaml_errors' + class="label label-danger has-tooltip" + :title='pipeline.yaml_errors' + :data-original-title='pipeline.yaml_errors' + > + yaml invalid + </span> + <span + v-if='pipeline.flags.stuck' + class="label label-warning" + > + stuck + </span> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 new file mode 100644 index 00000000000..b2ed05503c9 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -0,0 +1,131 @@ +/* global Vue, Turbolinks, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VuePipelines = Vue.extend({ + components: { + runningPipeline: gl.VueRunningPipeline, + pipelineActions: gl.VuePipelineActions, + stages: gl.VueStages, + commit: gl.CommitComponent, + pipelineUrl: gl.VuePipelineUrl, + pipelineHead: gl.VuePipelineHead, + glPagination: gl.VueGlPagination, + statusScope: gl.VueStatusScope, + timeAgo: gl.VueTimeAgo, + }, + data() { + return { + pipelines: [], + timeLoopInterval: '', + intervalId: '', + apiScope: 'all', + pageInfo: {}, + pagenum: 1, + count: { all: 0, running_or_pending: 0 }, + pageRequest: false, + }; + }, + props: ['scope', 'store', 'svgs'], + created() { + const pagenum = gl.utils.getParameterByName('p'); + const scope = gl.utils.getParameterByName('scope'); + if (pagenum) this.pagenum = pagenum; + if (scope) this.apiScope = scope; + this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope); + }, + methods: { + change(pagenum, apiScope) { + Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + }, + author(pipeline) { + if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; + if (pipeline.commit.author) return pipeline.commit.author; + return { + avatar_url: pipeline.commit.author_gravatar_url, + web_url: `mailto:${pipeline.commit.author_email}`, + username: pipeline.commit.author_name, + }; + }, + ref(pipeline) { + const { ref } = pipeline; + return { name: ref.name, tag: ref.tag, ref_url: ref.path }; + }, + commitTitle(pipeline) { + return pipeline.commit ? pipeline.commit.title : ''; + }, + commitSha(pipeline) { + return pipeline.commit ? pipeline.commit.short_id : ''; + }, + commitUrl(pipeline) { + return pipeline.commit ? pipeline.commit.commit_path : ''; + }, + match(string) { + return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); + }, + }, + template: ` + <div> + <div class="pipelines realtime-loading" v-if='pipelines.length < 1'> + <i class="fa fa-spinner fa-spin"></i> + </div> + <div class="table-holder" v-if='pipelines.length'> + <table class="table ci-table"> + <thead> + <tr> + <th class="pipeline-status">Status</th> + <th class="pipeline-info">Pipeline</th> + <th class="pipeline-commit">Commit</th> + <th class="pipeline-stages">Stages</th> + <th class="pipeline-date"></th> + <th class="pipeline-actions hidden-xs"></th> + </tr> + </thead> + <tbody> + <tr class="commit" v-for='pipeline in pipelines'> + <status-scope + :pipeline='pipeline' + :match='match' + :svgs='svgs' + > + </status-scope> + <pipeline-url :pipeline='pipeline'></pipeline-url> + <td> + <commit + :commit-icon-svg='svgs.commitIconSvg' + :author='author(pipeline)' + :tag="pipeline.ref.tag" + :title='commitTitle(pipeline)' + :commit-ref='ref(pipeline)' + :short-sha='commitSha(pipeline)' + :commit-url='commitUrl(pipeline)' + > + </commit> + </td> + <stages + :pipeline='pipeline' + :svgs='svgs' + :match='match' + > + </stages> + <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago> + <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions> + </tr> + </tbody> + </table> + </div> + <div class="pipelines realtime-loading" v-if='pageRequest'> + <i class="fa fa-spinner fa-spin"></i> + </div> + <gl-pagination + v-if='pageInfo.total > pageInfo.perPage' + :pagenum='pagenum' + :change='change' + :count='count.all' + :pageInfo='pageInfo' + > + </gl-pagination> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 new file mode 100644 index 00000000000..496df9aaced --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -0,0 +1,103 @@ +/* global Vue, Flash, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueStage = Vue.extend({ + data() { + return { + builds: '', + spinner: '<span class="fa fa-spinner fa-spin"></span>', + }; + }, + props: { + stage: { + type: Object, + required: true, + }, + svgs: { + type: DOMStringMap, + required: true, + }, + match: { + type: Function, + required: true, + }, + }, + methods: { + fetchBuilds(e) { + const areaExpanded = e.currentTarget.attributes['aria-expanded']; + + if (areaExpanded && (areaExpanded.textContent === 'true')) return null; + + return this.$http.get(this.stage.dropdown_path) + .then((response) => { + this.builds = JSON.parse(response.body).html; + }, () => { + const flash = new Flash('Something went wrong on our end.'); + return flash; + }); + }, + keepGraph(e) { + const { target } = e; + + if (target.className.indexOf('js-ci-action-icon') >= 0) return null; + + if ( + target.parentElement && + (target.parentElement.className.indexOf('js-ci-action-icon') >= 0) + ) return null; + + return e.stopPropagation(); + }, + }, + computed: { + buildsOrSpinner() { + return this.builds ? this.builds : this.spinner; + }, + dropdownClass() { + if (this.builds) return 'js-builds-dropdown-container'; + return 'js-builds-dropdown-loading builds-dropdown-loading'; + }, + buildStatus() { + return `Build: ${this.stage.status.label}`; + }, + tooltip() { + return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; + }, + svg() { + const { icon } = this.stage.status; + const stageIcon = icon.replace(/icon/i, 'stage_icon'); + return this.svgs[this.match(stageIcon)]; + }, + triggerButtonClass() { + return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`; + }, + }, + template: ` + <div> + <button + @click='fetchBuilds($event)' + :class="triggerButtonClass" + :title='stage.title' + data-placement="top" + data-toggle="dropdown" + type="button" + :aria-label='stage.title' + > + <span v-html="svg" aria-hidden="true"></span> + <i class="fa fa-caret-down" aria-hidden="true"></i> + </button> + <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> + <div class="arrow-up" aria-hidden="true"></div> + <div + @click='keepGraph($event)' + :class="dropdownClass" + class="js-builds-dropdown-list scrollable-menu" + v-html="buildsOrSpinner" + > + </div> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6 new file mode 100644 index 00000000000..cb176b3f0c6 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/stages.js.es6 @@ -0,0 +1,21 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueStages = Vue.extend({ + components: { + 'vue-stage': gl.VueStage, + }, + props: ['pipeline', 'svgs', 'match'], + template: ` + <td class="stage-cell"> + <div + class="stage-container dropdown js-mini-pipeline-graph" + v-for='stage in pipeline.details.stages' + > + <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage> + </div> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/status.js.es6 b/app/assets/javascripts/vue_pipelines_index/status.js.es6 new file mode 100644 index 00000000000..05175082704 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/status.js.es6 @@ -0,0 +1,34 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueStatusScope = Vue.extend({ + props: [ + 'pipeline', 'svgs', 'match', + ], + computed: { + cssClasses() { + const cssObject = { 'ci-status': true }; + cssObject[`ci-${this.pipeline.details.status.group}`] = true; + return cssObject; + }, + svg() { + return this.svgs[this.match(this.pipeline.details.status.icon)]; + }, + detailsPath() { + const { status } = this.pipeline.details; + return status.has_details ? status.details_path : false; + }, + }, + template: ` + <td class="commit-link"> + <a + :class='cssClasses' + :href='detailsPath' + v-html='svg + pipeline.details.status.text' + > + </a> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 new file mode 100644 index 00000000000..9e19b1564dc --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -0,0 +1,65 @@ +/* global gl, Flash */ +/* eslint-disable no-param-reassign, no-underscore-dangle */ +/*= require vue_realtime_listener/index.js */ + +((gl) => { + const pageValues = (headers) => { + const normalized = gl.utils.normalizeHeaders(headers); + + const paginationInfo = { + perPage: +normalized['X-PER-PAGE'], + page: +normalized['X-PAGE'], + total: +normalized['X-TOTAL'], + totalPages: +normalized['X-TOTAL-PAGES'], + nextPage: +normalized['X-NEXT-PAGE'], + previousPage: +normalized['X-PREV-PAGE'], + }; + + return paginationInfo; + }; + + gl.PipelineStore = class { + fetchDataLoop(Vue, pageNum, url, apiScope) { + const updatePipelineNums = (count) => { + const { all } = count; + const running = count.running_or_pending; + document.querySelector('.js-totalbuilds-count').innerHTML = all; + document.querySelector('.js-running-count').innerHTML = running; + }; + + const goFetch = () => + this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) + .then((response) => { + const pageInfo = pageValues(response.headers); + this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); + + const res = JSON.parse(response.body); + this.count = Object.assign({}, this.count, res.count); + this.pipelines = Object.assign([], this.pipelines, res.pipelines); + + updatePipelineNums(this.count); + this.pageRequest = false; + }, () => { + this.pageRequest = false; + return new Flash('Something went wrong on our end.'); + }); + + goFetch(); + + const startTimeLoops = () => { + this.timeLoopInterval = setInterval(() => { + this.$children + .filter(e => e.$options._componentTag === 'time-ago') + .forEach(e => e.changeTime()); + }, 10000); + }; + + startTimeLoops(); + + const removeIntervals = () => clearInterval(this.timeLoopInterval); + const startIntervals = () => startTimeLoops(); + + gl.VueRealtimeListener(removeIntervals, startIntervals); + } + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 new file mode 100644 index 00000000000..655110feba1 --- /dev/null +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -0,0 +1,73 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueTimeAgo = Vue.extend({ + data() { + return { + currentTime: new Date(), + }; + }, + props: ['pipeline', 'svgs'], + computed: { + timeAgo() { + return gl.utils.getTimeago(); + }, + localTimeFinished() { + return gl.utils.formatDate(this.pipeline.details.finished_at); + }, + timeStopped() { + const changeTime = this.currentTime; + const options = { + weekday: 'long', + year: 'numeric', + month: 'short', + day: 'numeric', + }; + options.timeZoneName = 'short'; + const finished = this.pipeline.details.finished_at; + if (!finished && changeTime) return false; + return ({ words: this.timeAgo.format(finished) }); + }, + duration() { + const { duration } = this.pipeline.details; + const date = new Date(duration * 1000); + + let hh = date.getUTCHours(); + let mm = date.getUTCMinutes(); + let ss = date.getSeconds(); + + if (hh < 10) hh = `0${hh}`; + if (mm < 10) mm = `0${mm}`; + if (ss < 10) ss = `0${ss}`; + + if (duration !== null) return `${hh}:${mm}:${ss}`; + return false; + }, + }, + methods: { + changeTime() { + this.currentTime = new Date(); + }, + }, + template: ` + <td> + <p class="duration" v-if='duration'> + <span v-html='svgs.iconTimer'></span> + {{duration}} + </p> + <p class="finished-at" v-if='timeStopped'> + <i class="fa fa-calendar"></i> + <time + data-toggle="tooltip" + data-placement="top" + data-container="body" + :data-original-title='localTimeFinished' + > + {{timeStopped.words}} + </time> + </p> + </td> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6 new file mode 100644 index 00000000000..23cac1466d2 --- /dev/null +++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6 @@ -0,0 +1,18 @@ +/* eslint-disable no-param-reassign */ + +((gl) => { + gl.VueRealtimeListener = (removeIntervals, startIntervals) => { + const removeAll = () => { + removeIntervals(); + window.removeEventListener('beforeunload', removeIntervals); + window.removeEventListener('focus', startIntervals); + window.removeEventListener('blur', removeIntervals); + document.removeEventListener('page:fetch', removeAll); + }; + + window.addEventListener('beforeunload', removeIntervals); + window.addEventListener('focus', startIntervals); + window.addEventListener('blur', removeIntervals); + document.addEventListener('page:fetch', removeAll); + }; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index e09b59dd5aa..a8b7be7ad06 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, padded-blocks, max-len */ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len */ /* global Dropzone */ /* global Mousetrap */ @@ -93,7 +93,5 @@ }; return ZenMode; - })(); - }).call(this); diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index f1d36efb3de..8d38fc78a19 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -50,3 +50,77 @@ .pulse { @include webkit-prefix(animation-name, pulse); } + +/* +* General hover animations +*/ + + +// Sass multiple transitions mixin | https://gist.github.com/tobiasahlin/7a421fb9306a4f518aab +// Usage: @include transition(width, height 0.3s ease-in-out); +// Output: -webkit-transition(width 0.2s, height 0.3s ease-in-out); +// transition(width 0.2s, height 0.3s ease-in-out); +// +// Pass in any number of transitions +@mixin transition($transitions...) { + $unfoldedTransitions: (); + @each $transition in $transitions { + $unfoldedTransitions: append($unfoldedTransitions, unfoldTransition($transition), comma); + } + + transition: $unfoldedTransitions; +} + +@function unfoldTransition ($transition) { + // Default values + $property: all; + $duration: $general-hover-transition-duration; + $easing: $general-hover-transition-curve; // Browser default is ease, which is what we want + $delay: null; // Browser default is 0, which is what we want + $defaultProperties: ($property, $duration, $easing, $delay); + + // Grab transition properties if they exist + $unfoldedTransition: (); + @for $i from 1 through length($defaultProperties) { + $p: null; + @if $i <= length($transition) { + $p: nth($transition, $i); + } @else { + $p: nth($defaultProperties, $i); + } + $unfoldedTransition: append($unfoldedTransition, $p); + } + + @return $unfoldedTransition; +} + +.btn, +.side-nav-toggle { + @include transition(background-color, border-color, color, box-shadow); +} + +.dropdown-menu-toggle, +.avatar-circle, +.header-user-avatar { + @include transition(border-color); +} + +.note-action-button .link-highlight, +.toolbar-btn, +.dropdown-toggle-caret, +.fa:not(.fa-bell) { + @include transition(color); +} + +a { + @include transition(background-color, color, border); +} + +.tree-table td, +.well-list > li { + @include transition(background-color, border-color); +} + +.stage-nav-item { + @include transition(background-color, box-shadow); +} diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 48827578d94..1d59700543c 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -37,6 +37,8 @@ display: inline-block; margin-left: 4px; margin-bottom: 2px; + flex-shrink: 0; + -webkit-flex-shrink: 0; &.s16 { margin-right: 4px; } &.s24 { margin-right: 4px; } @@ -52,6 +54,10 @@ border-radius: 0; border: none; } + + &:not([href]):hover { + border-color: rgba($avatar-border, .2); + } } .identicon { diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 9fc9bcebc44..49907417e26 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -97,8 +97,20 @@ padding: 5px 6px; outline: 0; - &:hover, + &.disabled { + cursor: default; + + &:hover, + &:focus, + &:active { + background-color: $white-light; + border-color: $border-color; + box-shadow: none; + } + } + &.active, + &:hover, &:active { background-color: $row-hover; border-color: $row-hover-border; diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss index e9d7cda0647..47a8f44c709 100644 --- a/app/assets/stylesheets/framework/badges.scss +++ b/app/assets/stylesheets/framework/badges.scss @@ -4,8 +4,3 @@ color: $badge-color; vertical-align: baseline; } - -.badge-dark { - background-color: $badge-bg-dark; - color: $badge-color-dark; -} diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index e9aadffc73c..592ef0d647f 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -1,13 +1,13 @@ .centered-light-block { text-align: center; - color: $gl-gray; + color: $gl-text-color; margin: 20px; } .nothing-here-block { text-align: center; padding: 20px; - color: $gl-gray; + color: $gl-text-color; font-weight: normal; font-size: 14px; line-height: 36px; @@ -29,7 +29,7 @@ margin-bottom: 0; border-top: 1px solid $white-dark; border-bottom: 1px solid $white-dark; - color: $gl-gray; + color: $gl-text-color; &.oneline-block { line-height: 42px; @@ -82,7 +82,12 @@ } .block-controls { - float: right; + display: -webkit-flex; + display: flex; + -webkit-justify-content: flex-end; + justify-content: flex-end; + -webkit-flex: 1; + flex: 1; .control { float: left; @@ -135,11 +140,11 @@ } .cover-title { - color: $gl-header-color; + color: $gl-text-color; font-size: 23px; h1 { - color: $gl-gray-dark; + color: $gl-text-color; margin-bottom: 6px; font-size: 23px; } @@ -153,7 +158,7 @@ p { padding: 0 $gl-padding; - color: $gl-text-color-dark; + color: $gl-text-color; } } @@ -211,7 +216,7 @@ display: inline; font-weight: normal; font-size: 24px; - color: $gl-title-color; + color: $gl-text-color; } } } @@ -282,3 +287,8 @@ } } } + +.flex-container-block { + display: -webkit-flex; + display: flex; +} diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index a11f1cd7735..bb6129158d9 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -88,7 +88,7 @@ } @mixin btn-gray { - @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark); + @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-text-color); } @mixin btn-white { @@ -242,7 +242,7 @@ } .btn-transparent { - color: $gl-gray-light; + color: $gl-text-color-secondary; background-color: transparent; border: 0; @@ -324,7 +324,7 @@ &:focus { cursor: text; box-shadow: none; - border-color: $border-color; + border-color: lighten($dropdown-input-focus-border, 20%); color: $gray-darkest; background-color: $gray-light; } @@ -338,7 +338,7 @@ margin-left: 10px; i { - color: $gl-gray-light; + color: $gl-text-color-secondary; } } diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index ef921a8c6a9..1d2d1bfc0d7 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -1,6 +1,7 @@ .calender-block { padding-left: 0; padding-right: 0; + direction: rtl; @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { overflow-x: scroll; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 67b5aa37ae7..0ce94a26a7f 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -412,7 +412,7 @@ table { padding: 0 10px; clip: auto; text-decoration: none; - color: $gl-title-color; + color: $gl-text-color; background: $gray-light; z-index: 1; } diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 889366d6ddf..6bfb9a6d1cb 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -125,7 +125,8 @@ top: 100%; left: 0; z-index: 9; - width: 240px; + max-width: 280px; + min-width: 240px; margin-top: 2px; margin-bottom: 0; font-size: 14px; @@ -201,7 +202,7 @@ } .icon-play { - fill: $gl-gray-light; + fill: $gl-text-color-secondary; margin-right: 6px; height: 12px; width: 11px; @@ -209,7 +210,7 @@ } .dropdown-header { - color: $gl-gray-light; + color: $gl-text-color-secondary; font-size: 13px; line-height: 22px; padding: 0 10px; @@ -222,7 +223,7 @@ .unclickable { cursor: not-allowed; padding: 5px 8px; - color: $gl-gray-light; + color: $gl-text-color-secondary; } } @@ -592,7 +593,7 @@ } .ui-datepicker-title { - color: $gl-gray; + color: $gl-text-color; font-size: 14px; line-height: 1; font-weight: normal; @@ -614,17 +615,17 @@ .dropdown-menu-inner-title { display: block; - color: $gl-title-color; + color: $gl-text-color; font-weight: 600; } .dropdown-menu-inner-content { display: block; - color: $gl-gray-light; + color: $gl-text-color-secondary; } .dropdown-toggle-text { &.is-default { - color: $gl-gray-light; + color: $gl-text-color-secondary; } } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 88ed0a4a17e..c51912b4ac4 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -182,3 +182,52 @@ span.idiff { border-bottom-right-radius: 2px; } } + +.file-stats { + ul { + list-style: none; + margin: 0; + padding: 10px 0; + + li { + padding: 3px 0; + line-height: 20px; + } + } + + .new-file { + a { + color: $gl-text-green; + } + } + + .renamed-file { + a { + color: $gl-text-orange; + } + } + + .deleted-file { + a { + color: $gl-text-red; + } + } + + .edit-file { + a { + color: $gl-text-color; + } + } + + a { + text-decoration: none; + + .new-file { + color: $notify-new-file; + } + + .deleted-file { + color: $notify-deleted-file; + } + } +} diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 19827943385..e3da467a27c 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -23,3 +23,129 @@ } } +.filtered-search-container { + display: -webkit-flex; + display: flex; +} + +.filtered-search-input-container { + display: -webkit-flex; + display: flex; + position: relative; + width: 100%; + + .form-control { + padding-left: 25px; + padding-right: 25px; + + &:focus ~ .fa-filter { + color: $common-gray-dark; + } + } + + .fa-filter { + position: absolute; + top: 10px; + left: 10px; + color: $gray-darkest; + } + + .fa-times { + right: 10px; + color: $gray-darkest; + } + + .clear-search { + width: 35px; + background-color: transparent; + border: none; + position: absolute; + right: 0; + height: 100%; + outline: none; + + &:hover .fa-times { + color: $common-gray-dark; + } + } +} + +.dropdown-menu .filter-dropdown-item { + padding: 0; +} + +.filter-dropdown { + max-height: 215px; + overflow: auto; +} + +%filter-dropdown-item-btn-hover { + background-color: $dropdown-hover-color; + color: $white-light; + text-decoration: none; + + .avatar { + border-color: $white-light; + } +} + +.filter-dropdown-item { + .btn { + border: none; + width: 100%; + text-align: left; + padding: 8px 16px; + text-overflow: ellipsis; + overflow: hidden; + border-radius: 0; + + .fa { + width: 15px; + } + + .dropdown-label-box { + border-color: $white-light; + border-style: solid; + border-width: 1px; + width: 17px; + height: 17px; + } + + &:hover, + &:focus { + @extend %filter-dropdown-item-btn-hover; + } + } + + .dropdown-light-content { + font-size: 14px; + font-weight: 400; + } + + .dropdown-user { + display: -webkit-flex; + display: flex; + } + + .dropdown-user-details { + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; + + &> span { + white-space: normal; + word-break: break-all; + } + } +} + +.filter-dropdown-item.dropdown-active { + .btn { + @extend %filter-dropdown-item-btn-hover; + } +} + +.filter-dropdown-loading { + padding: 8px 16px; +} diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 8726a69867b..25d6fbe465a 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -153,7 +153,7 @@ label { } .form-control::-webkit-input-placeholder { - color: $gl-gray-light; + color: $gl-text-color-secondary; } .input-group { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 971940773f7..2a01bc4d44d 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -45,7 +45,7 @@ header { padding: 0; .nav > li > a { - color: $gl-gray-light; + color: $gl-text-color-secondary; font-size: 18px; padding: 0; margin: ($header-height - 28) / 2 0; @@ -57,13 +57,21 @@ header { &.header-user-dropdown-toggle { margin-left: 14px; + + &:hover, + &:focus, + &:active { + .header-user-avatar { + border-color: rgba($avatar-border, .2); + } + } } &:hover, &:focus, &:active { background-color: $gray-light; - color: darken($gl-gray-light, 30%); + color: $gl-text-color; .todos-pending-count { background: darken($todo-alert-blue, 10%); @@ -88,7 +96,7 @@ header { } &.active { - color: $gl-gray-light; + color: $gl-text-color-secondary; } } } @@ -104,6 +112,7 @@ header { &:hover { background-color: $white-normal; + color: $gl-header-nav-hover-color; } } } @@ -180,6 +189,7 @@ header { &:hover { text-decoration: underline; + color: $gl-header-nav-hover-color; } } @@ -198,7 +208,7 @@ header { cursor: pointer; &:hover { - color: darken($color: $gl-text-color, $amount: 30%); + color: $gl-header-nav-hover-color; } } @@ -271,4 +281,5 @@ header { float: left; margin-right: 5px; border-radius: 50%; + border: 1px solid $avatar-border; } diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 8624a25c052..868f28cd356 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -15,6 +15,7 @@ } .ci-status-icon-pending, +.ci-status-icon-failed_with_warnings, .ci-status-icon-success_with_warnings { color: $gl-warning; @@ -34,10 +35,10 @@ .ci-status-icon-canceled, .ci-status-icon-disabled, .ci-status-icon-not-found { - color: $gl-gray; + color: $gl-text-color; svg { - fill: $gl-gray; + fill: $gl-text-color; } } diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 298913108ee..46632f15f35 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -41,6 +41,6 @@ } &.status-box-upcoming { - background: $gl-gray-light; + background: $gl-text-color-secondary; } } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 5365b62e456..29d55c44699 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -41,6 +41,21 @@ body { } } + .alert-link-group { + float: right; + } + + /* Center alert text and alert action links on smaller screens */ + @media (max-width: $screen-sm-max) { + .alert { + text-align: center; + } + + .alert-link-group { + float: none; + } + } + /* 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; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 277d4202950..426596027de 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -128,7 +128,7 @@ ul.content-list { } a { - color: $gl-dark-link-color; + color: $gl-text-color; } .member-group-link { @@ -155,7 +155,8 @@ ul.content-list { } > .btn, - > .btn-group { + > .btn-group, + > .dropdown.inline { margin-right: $gl-padding-top; display: inline-block; margin-top: 3px; @@ -163,6 +164,10 @@ ul.content-list { &:last-child { margin-right: 0; + + @media(max-width: $screen-xs-max) { + margin: 0 auto; + } } } @@ -230,20 +235,52 @@ ul.content-list { } .label-default { - color: $gl-gray-light; + color: $gl-text-color-secondary; } } -.panel > .content-list > li { - padding: $gl-padding-top $gl-padding; +// Table list +.table-list { + display: table; + width: 100%; + + .table-list-row { + display: table-row; + } + + .table-list-cell { + display: table-cell; + vertical-align: top; + padding: 10px 16px; + border-bottom: 1px solid $gray-darker; + + &.avatar-cell { + width: 36px; + padding-right: 0; + + img { + margin-right: 0; + } + } + } + + &.table-wide { + .table-list-cell { + &:last-of-type { + padding-right: 0; + } - &.commit { - @media (min-width: $screen-sm-min) { - padding-left: 46px + $gl-padding; + &:first-of-type { + padding-left: 0; + } } } } +.panel > .content-list > li { + padding: $gl-padding-top $gl-padding; +} + ul.controls { float: right; list-style: none; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index e30d81d09f0..5bff694658c 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -73,7 +73,7 @@ } .referenced-users { - color: $gl-header-color; + color: $gl-text-color; padding-top: 10px; } @@ -135,7 +135,7 @@ .toolbar-btn { float: left; padding: 0 5px; - color: $gl-gray-light; + color: $gl-text-color-secondary; background: transparent; border: 0; outline: 0; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 4f2ac77f228..1acd06122a3 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -46,7 +46,7 @@ &.light { a { - color: $gl-gray; + color: $gl-text-color; } } } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 7eb9962ba33..8e2c56a8488 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -23,21 +23,21 @@ margin-right: 0; } - .issues-details-filters, + .issues-details-filters:not(.filtered-search-block), .dash-projects-filters, .check-all-holder { display: none; } - .rss-btn { + .issues-holder .issue-check { display: none; } - .project-home-links { + .rss-btn { display: none; } - .project-avatar { + .project-home-links { display: none; } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 5abda13a963..401c2d0f6ee 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -51,7 +51,7 @@ margin-bottom: -1px; font-size: 14px; line-height: 28px; - color: $gl-gray-light; + color: $gl-text-color-secondary; border-bottom: 2px solid transparent; &:hover, @@ -101,7 +101,7 @@ &:hover, &:active, &:focus { - border-bottom: none; + border-color: transparent; } } } @@ -315,7 +315,7 @@ .fa-caret-down { margin-left: 5px; - color: $gl-gray-light; + color: $gl-text-color-secondary; } .dropdown { diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss index 625bea96aaa..5f4211147f3 100644 --- a/app/assets/stylesheets/framework/page-header.scss +++ b/app/assets/stylesheets/framework/page-header.scss @@ -14,7 +14,7 @@ .header-action-buttons { i { - color: $gl-gray-light; + color: $gl-text-color-secondary; font-size: 13px; margin-right: 3px; } @@ -42,14 +42,10 @@ .commit-committer-link, .commit-author-link { - color: $gl-gray; + color: $gl-text-color; font-weight: bold; } - .fa-clipboard { - color: $dropdown-title-btn-color; - } - .commit-info { &.branches { margin-left: 8px; diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 9d8d08dff88..efe93724013 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -48,3 +48,11 @@ line-height: inherit; } } + +.panel-default { + .table-list-row:last-child { + .table-list-cell { + border-bottom: 0; + } + } +} diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index a8641e83154..f0b03710c79 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -183,7 +183,9 @@ &.right-sidebar-expanded { .line-resolve-all-container { - display: none; + @media (min-width: $sidebar-breakpoint) { + display: none; + } } } } @@ -234,9 +236,13 @@ header.header-sidebar-pinned { @media (min-width: $screen-md-min) { padding-right: $gutter_width; - .merge-request-tabs-holder.affix { + &:not(.with-overlay) .merge-request-tabs-holder.affix { right: $gutter_width; } + + &.with-overlay .merge-request-tabs-holder.affix { + right: $sidebar_collapsed_width; + } } &.with-overlay { diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 6078505807e..ff185cd8767 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -7,7 +7,7 @@ .timeline-entry { padding: $gl-padding $gl-btn-padding 11px; border-color: $white-normal; - color: $gl-gray; + color: $gl-text-color; border-bottom: 1px solid $border-white-light; &:target { @@ -32,7 +32,7 @@ .system-note { .note-text { - color: $gl-gray !important; + color: $gl-text-color !important; } } diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss index 28cbae9a449..12d56359d7d 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap.scss @@ -98,7 +98,7 @@ &.label-gray { background-color: $label-gray-bg; - color: $gl-gray; + color: $gl-text-color; text-shadow: none; } diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss index 876adf7f712..0fc89d5976a 100644 --- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss +++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss @@ -65,11 +65,11 @@ $legend-color: $text-color; // //## -$pagination-color: $gl-gray; +$pagination-color: $gl-text-color; $pagination-bg: $white-light; $pagination-border: $border-color; -$pagination-hover-color: $gl-gray; +$pagination-hover-color: $gl-text-color; $pagination-hover-bg: $row-hover; $pagination-hover-border: $border-color; @@ -121,6 +121,9 @@ $panel-default-heading-bg: $gray-light; $panel-footer-bg: $gray-light; $panel-inner-border: $border-color; +$badge-bg: $badge-bg; +$badge-color: $badge-color; + //== Wells // //## @@ -154,7 +157,7 @@ $nav-link-padding: 13px $gl-padding; // //## $pre-bg: $gray-light !default; -$pre-color: $gl-gray !default; +$pre-color: $gl-text-color !default; $pre-border-color: $border-color; $table-bg-accent: $gray-light; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index d906d26bba9..54958973f15 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -1,5 +1,5 @@ @mixin md-typography { - color: $md-text-color; + color: $gl-text-color; word-wrap: break-word; a { @@ -10,7 +10,7 @@ max-width: 100%; } - *:first-child { + *:first-child:not(.katex-display) { margin-top: 0; } @@ -50,14 +50,14 @@ margin: 16px 0 10px; padding: 0 0 0.3em; border-bottom: 1px solid $white-dark; - color: $gl-gray-dark; + color: $gl-text-color; } h2 { font-size: 1.5em; font-weight: 600; margin: 16px 0 10px; - color: $gl-gray-dark; + color: $gl-text-color; } h3 { @@ -100,7 +100,7 @@ } p { - color: $gl-text-color-dark; + color: $gl-text-color; margin: 6px 0 0; } @@ -108,7 +108,7 @@ @extend .table; @extend .table-bordered; margin: 12px 0; - color: $gl-text-color-dark; + color: $gl-text-color; th { background: $label-gray-bg; @@ -230,7 +230,7 @@ h3, h4, h5, h6 { - color: $gl-title-color; + color: $gl-text-color; font-weight: 600; } @@ -292,7 +292,7 @@ h2, h3, h4 { small { - color: $gl-gray; + color: $gl-text-color; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index b0c4a6edf57..7809d4866f1 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -5,7 +5,7 @@ $sidebar_collapsed_width: 62px; $sidebar_width: 220px; $gutter_collapsed_width: 62px; $gutter_width: 290px; -$gutter_inner_width: 258px; +$gutter_inner_width: 250px; $sidebar-transition-duration: .15s; $sidebar-breakpoint: 1024px; @@ -56,6 +56,7 @@ $black-transparent: rgba(0, 0, 0, 0.3); $border-white-light: darken($white-light, $darken-border-factor); $border-white-normal: darken($white-normal, $darken-border-factor); +$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); @@ -85,6 +86,7 @@ $warning-message-border: #f0e2bb; */ $border-color: #e5e5e5; $focus-border-color: #3aabf0; +$sidebar-collapsed-icon-color: #999; $well-expand-item: #e8f2f7; $well-inner-border: #eef0f2; $well-light-border: #f1f1f1; @@ -94,29 +96,26 @@ $well-light-text-color: #5b6169; * Text */ $gl-font-size: 14px; -$gl-title-color: #333; -$gl-text-color: #5c5c5c; -$gl-text-color-dark: #5c5d5e; -$gl-text-color-light: #8c8c8c; +$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-diff-text-color: #555; -$gl-dark-link-color: #333; -$gl-gray-light: #8f8f8f; $gl-grayish-blue: #7f8fa4; $gl-gray: $gl-text-color; $gl-gray-dark: #313236; $gl-header-color: #4c4e54; +$gl-header-nav-hover-color: #434343; /* * Lists */ $list-font-size: $gl-font-size; -$list-title-color: $gl-title-color; +$list-title-color: $gl-text-color; $list-text-color: $gl-text-color; -$list-text-disabled-color: #888; +$list-text-disabled-color: $gl-text-color-disabled; $list-border-light: #eee; $list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; @@ -127,7 +126,6 @@ $list-warning-row-color: #8a6d3b; /* * Markdown */ -$md-text-color: $gl-text-color; $md-link-color: $gl-link-color; $md-area-border: #ddd; @@ -168,9 +166,7 @@ $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; $issue-status-expired: #cea61b; -$issuable-sidebar-color: #999; -$issuable-avatar-hover-border: #999; -$issuable-clipboard-color: #999; +$issuable-sidebar-color: $gl-text-color-secondary; $show-aside-bg: #eee; $show-aside-color: #777; $show-aside-shadow: #ddd; @@ -182,6 +178,9 @@ $count-arrow-border: #dce0e5; $save-project-loader-color: #555; $divergence-graph-bar-bg: #ccc; $divergence-graph-separator-bg: #ccc; +$general-hover-transition-duration: 100ms; +$general-hover-transition-curve: linear; + /* * Common component specific colors @@ -274,18 +273,22 @@ $dropdown-toggle-active-border-color: darken($border-color, 14%); /* +* Filtered Search +*/ +$dropdown-hover-color: #3b86ff; + +/* * Buttons */ $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; +$btn-white-active: #848484; /* * Badges */ -$badge-bg: #f3f3f3; -$badge-bg-dark: #eee; -$badge-color: #929292; -$badge-color-dark: #8f8f8f; +$badge-bg: #eee; +$badge-color: $gl-text-color-secondary; /* * Award emoji @@ -304,8 +307,8 @@ $location-icon-color: #e7e9ed; /* * Notes */ -$notes-light-color: #8e8e8e; -$notes-role-color: #8e8e8e; +$notes-light-color: $gl-text-color-secondary; +$notes-role-color: $gl-text-color-secondary; $note-disabled-comment-color: #b2b2b2; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; @@ -330,7 +333,7 @@ $calendar-user-contrib-text: #959494; $cycle-analytics-box-padding: 30px; $cycle-analytics-box-text-color: #8c8c8c; $cycle-analytics-big-font: 19px; -$cycle-analytics-dark-text: $gl-title-color; +$cycle-analytics-dark-text: $gl-text-color; $cycle-analytics-light-gray: #bfbfbf; $cycle-analytics-dismiss-icon-color: #b2b2b2; @@ -376,14 +379,13 @@ $callout-success-color: #3c763d; /* * Commit Page */ -$commit-committer-color: #999; $commit-max-width-marker-color: rgba(0, 0, 0, 0.0); $commit-message-text-area-bg: rgba(0, 0, 0, 0.0); /* * Common */ -$common-gray: $gl-gray; +$common-gray: $gl-text-color; $common-gray-light: #bbb; $common-gray-dark: #444; $common-red: $gl-text-red; @@ -434,6 +436,7 @@ $help-shortcut-header-color: #333; */ $issues-today-bg: #f3fff2; $issues-today-border: #e1e8d5; +$compare-display-color: #888; /* * jQuery UI @@ -538,3 +541,4 @@ Pipeline Graph */ $stage-hover-bg: #eaf3fc; $stage-hover-border: #d1e7fc; +$action-icon-color: #d6d6d6;
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss index f9c850ebdc8..32eb750180f 100644 --- a/app/assets/stylesheets/framework/wells.scss +++ b/app/assets/stylesheets/framework/wells.scss @@ -1,6 +1,6 @@ .info-well { background: $gray-light; - color: $gl-gray; + color: $gl-text-color; border: 1px solid $border-color; border-radius: $border-radius-default; diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index 84b639fabf5..97ade638db6 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -40,7 +40,7 @@ } .zen-control-full { - color: $gl-gray-light; + color: $gl-text-color-secondary; &:hover { color: $gl-link-color; diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index cb923166b25..6f2e746d4b0 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -13,6 +13,8 @@ $dark-main-bg: #1d1f21; $dark-main-color: #1d1f21; $dark-line-color: #c5c8c6; $dark-line-num-color: rgba(255, 255, 255, 0.3); +$dark-line-num-color-new: #627165; +$dark-line-num-color-old: #806565; $dark-diff-not-empty-bg: #557; $dark-highlight-bg: #ffe792; $dark-highlight-color: $black; @@ -89,7 +91,6 @@ $dark-il: #de935f; .diff-line-num, .diff-line-num a { - color: $dark-main-color; color: $dark-line-num-color; } @@ -121,11 +122,21 @@ $dark-il: #de935f; .diff-line-num.new, .line_content.new { @include diff_background($dark-new-bg, $dark-new-idiff, $dark-border); + + &::before, + a { + color: $dark-line-num-color-new; + } } .diff-line-num.old, .line_content.old { @include diff_background($dark-old-bg, $dark-old-idiff, $dark-border); + + &::before, + a { + color: $dark-line-num-color-old; + } } .line_content.match { diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index d8510baad8a..2144a5f7466 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -7,6 +7,8 @@ $monokai-bg: #272822; $monokai-border: #555; $monokai-text-color: #f8f8f2; $monokai-line-num-color: rgba(255, 255, 255, 0.3); +$monokai-line-num-color-new: #707565; +$monokai-line-num-color-old: #7e736f; $monokai-line-empty-bg: #49483e; $monokai-line-empty-border: darken($monokai-line-empty-bg, 15%); $monokai-diff-border: #808080; @@ -120,11 +122,21 @@ $monokai-gi: #a6e22e; .diff-line-num.new, .line_content.new { @include diff_background($monokai-new-bg, $monokai-new-idiff, $monokai-diff-border); + + &::before, + a { + color: $monokai-line-num-color-new; + } } .diff-line-num.old, .line_content.old { @include diff_background($monokai-old-bg, $monokai-old-idiff, $monokai-diff-border); + + &::before, + a { + color: $monokai-line-num-color-old; + } } .line_content.match { diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index 874aecb5e16..2cb1d18f12f 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -13,6 +13,8 @@ $solarized-dark-pre-color: #93a1a1; $solarized-dark-pre-border: #113b46; $solarized-dark-line-bg: #002b36; $solarized-dark-line-color: rgba(255, 255, 255, 0.3); +$solarized-dark-line-color-new: #5a766c; +$solarized-dark-line-color-old: #7a6c71; $solarized-dark-highlight: #094554; $solarized-dark-hll-bg: #174652; $solarized-dark-c: #586e75; @@ -124,11 +126,21 @@ $solarized-dark-il: #2aa198; .diff-line-num.new, .line_content.new { @include diff_background($solarized-dark-new-bg, $solarized-dark-new-idiff, $solarized-dark-border); + + &::before, + a { + color: $solarized-dark-line-color-new; + } } .diff-line-num.old, .line_content.old { @include diff_background($solarized-dark-old-bg, $solarized-dark-old-idiff, $solarized-dark-border); + + &::before, + a { + color: $solarized-dark-line-color-old; + } } .line_content.match { diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 499a1c108b8..b72c4326730 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -13,6 +13,9 @@ $solarized-light-pre-bg: #002b36; $solarized-light-pre-bg: #fdf6e3; $solarized-light-pre-color: #586e75; $solarized-light-line-bg: #fdf6e3; +$solarized-light-line-color: rgba(0, 0, 0, 0.3); +$solarized-light-line-color-new: #a1a080; +$solarized-light-line-color-old: #ad9186; $solarized-light-highlight: #eee8d5; $solarized-light-hll-bg: #ddd8c5; $solarized-light-c: #93a1a1; @@ -98,7 +101,7 @@ $solarized-light-il: #2aa198; .diff-line-num, .diff-line-num a { - color: $black-transparent; + color: $solarized-light-line-color; } // Code itself @@ -130,11 +133,21 @@ $solarized-light-il: #2aa198; .line_content.new { @include diff_background($solarized-light-new-bg, $solarized-light-new-idiff, $solarized-light-border); + + &::before, + a { + color: $solarized-light-line-color-new; + } } .diff-line-num.old, .line_content.old { @include diff_background($solarized-light-old-bg, $solarized-light-old-idiff, $solarized-light-border); + + &::before, + a { + color: $solarized-light-line-color-old; + } } .line_content.match { diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index 54a5664a874..398fbfd3b18 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -3,7 +3,7 @@ /* * White Syntax Colors */ -$white-code-color: #333; +$white-code-color: $gl-text-color; $white-highlight: #fafe3d; $white-pre-hll-bg: #f8eec7; $white-hll-bg: #f8f8f8; @@ -108,11 +108,19 @@ $white-gc-bg: #eaf2f5; &.old { background-color: $line-number-old; border-color: $line-removed-dark; + + a { + color: scale-color($line-number-old,$red: -30%, $green: -30%, $blue: -30%); + } } &.new { background-color: $line-number-new; border-color: $line-added-dark; + + a { + color: scale-color($line-number-new,$red: -30%, $green: -30%, $blue: -30%); + } } &.hll:not(.empty-cell) { @@ -125,6 +133,10 @@ $white-gc-bg: #eaf2f5; &.old { background-color: $line-removed; + &::before { + color: scale-color($line-number-old,$red: -30%, $green: -30%, $blue: -30%); + } + span.idiff { background-color: $line-removed-dark; } @@ -133,6 +145,10 @@ $white-gc-bg: #eaf2f5; &.new { background-color: $line-added; + &::before { + color: scale-color($line-number-new,$red: -30%, $green: -30%, $blue: -30%); + } + span.idiff { background-color: $line-added-dark; } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index ddc382362f7..a81e5eb5ebf 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -18,15 +18,3 @@ p.details { pre.commit-message { white-space: pre-wrap; } - -.file-stats > a { - text-decoration: none; - - > .new-file { - color: $notify-new-file; - } - - > .deleted-file { - color: $notify-deleted-file; - } -} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 76a88d96183..f2d60bff2b5 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -74,6 +74,7 @@ height: 475px; // Needed for PhantomJS height: calc(100vh - 220px); min-height: 475px; + transition: width .2s; &.is-compact { width: calc(100% - 290px); @@ -259,7 +260,7 @@ .board-list-count { padding: 10px 0; - color: $gl-gray-light; + color: $gl-text-color-secondary; font-size: 13px; > .fa { @@ -338,3 +339,18 @@ } } } + +.right-sidebar.right-sidebar-expanded { + &.boards-sidebar-slide-enter-active, + &.boards-sidebar-slide-leave-active { + transition: width .2s, + padding .2s; + } + + &.boards-sidebar-slide-enter, + &.boards-sidebar-slide-leave-active { + width: 0; + padding-left: 0; + padding-right: 0; + } +} diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss new file mode 100644 index 00000000000..3e2fa8ca88d --- /dev/null +++ b/app/assets/stylesheets/pages/branches.scss @@ -0,0 +1,55 @@ +.divergence-graph { + padding: 12px 12px 0 0; + float: right; + + .graph-side { + position: relative; + width: 80px; + height: 22px; + padding: 5px 0 13px; + float: left; + + .bar { + position: absolute; + height: 4px; + background-color: $divergence-graph-bar-bg; + } + + .bar-behind { + right: 0; + border-radius: 3px 0 0 3px; + } + + .bar-ahead { + left: 0; + border-radius: 0 3px 3px 0; + } + + .count { + padding-top: 6px; + padding-bottom: 0; + font-size: 12px; + color: $gl-text-color; + display: block; + } + + .count-behind { + padding-right: 4px; + text-align: right; + } + + .count-ahead { + padding-left: 4px; + text-align: left; + } + } + + .graph-separator { + position: relative; + width: 1px; + height: 18px; + margin: 5px 0 0; + float: left; + background-color: $divergence-graph-separator-bg; + } +} diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index f9e8d297c05..fd101d43b5b 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -160,7 +160,7 @@ flex: 1; a { - color: $gl-gray; + color: $gl-text-color; &:hover { color: $gl-link-color; @@ -357,7 +357,7 @@ } .build-light-text { - color: $gl-gray-light; + color: $gl-text-color-secondary; } .build-gutter-toggle { diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss index d1cd1e5d848..90643832390 100644 --- a/app/assets/stylesheets/pages/ci_projects.scss +++ b/app/assets/stylesheets/pages/ci_projects.scss @@ -18,7 +18,7 @@ } td { - color: $gl-gray; + color: $gl-text-color; vertical-align: middle !important; a { diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss deleted file mode 100644 index bf656d0e28e..00000000000 --- a/app/assets/stylesheets/pages/commit.scss +++ /dev/null @@ -1,132 +0,0 @@ -.commit-title { - display: block; -} - -.commit-author, -.commit-committer { - display: block; - color: $commit-committer-color; - font-weight: normal; - font-style: italic; -} - -.commit-author strong, -.commit-committer strong { - font-weight: bold; - font-style: normal; -} - -.commit-description { - background: none; - border: none; - margin: 0; - padding: 0; - margin-top: 10px; - word-break: normal; - white-space: pre-wrap; -} - -.js-details-expand { - &:hover { - text-decoration: none; - } -} - -.ci-status-link { - svg { - overflow: visible; - } -} - -.commit-box { - border-top: 1px solid $border-color; - padding: $gl-padding 0; - - .commit-title { - margin: 0; - font-size: 23px; - color: $gl-gray-dark; - } - - .commit-description { - margin-top: 15px; - } -} - -.commit-hash-full { - @media (max-width: $screen-sm-max) { - width: 80px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: inline-block; - vertical-align: bottom; - } -} - -.file-stats { - ul { - list-style: none; - margin: 0; - padding: 10px 0; - - li { - padding: 3px 0; - line-height: 20px; - } - } - - .new-file { - a { - color: $gl-text-green; - } - } - - .renamed-file { - a { - color: $gl-text-orange; - } - } - - .deleted-file { - a { - color: $gl-text-red; - } - } - - .edit-file { - a { - color: $gl-text-color; - } - } -} - -/* - * Commit message textarea for web editor and - * custom merge request message - */ -.commit-message-container { - background-color: $body-bg; - position: relative; - font-family: $monospace_font; - $left: 12px; - overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 - .max-width-marker { - width: 72ch; - color: $commit-max-width-marker-color; - font-family: inherit; - left: $left; - height: 100%; - border-right: 1px solid mix($input-border, $white-light); - position: absolute; - z-index: 1; - } - - > textarea { - background-color: $commit-message-text-area-bg; - font-family: inherit; - padding-left: $left; - position: relative; - z-index: 2; - } -} diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index e76e1a73b25..fef8e8eec27 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,6 +1,75 @@ +.commit-description { + background: none; + border: none; + padding: 0; + margin-top: 10px; + word-break: normal; + white-space: pre-wrap; +} + +.js-details-expand { + &:hover { + text-decoration: none; + } +} + +.commit-box { + border-top: 1px solid $border-color; + padding: $gl-padding 0; + + .commit-title { + margin: 0; + color: $gl-text-color; + } + + .commit-description { + margin-top: 15px; + } +} + +.commit-hash-full { + @media (max-width: $screen-sm-max) { + width: 80px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + vertical-align: bottom; + } +} + +/* + * Commit message textarea for web editor and + * custom merge request message + */ +.commit-message-container { + background-color: $body-bg; + position: relative; + font-family: $monospace_font; + $left: 12px; + overflow: hidden; // See https://gitlab.com/gitlab-org/gitlab-ce/issues/13987 + .max-width-marker { + width: 72ch; + color: $commit-max-width-marker-color; + font-family: inherit; + left: $left; + height: 100%; + border-right: 1px solid mix($input-border, $white-light); + position: absolute; + z-index: 1; + } + + textarea { + background-color: $commit-message-text-area-bg; + font-family: inherit; + padding-left: $left; + position: relative; + z-index: 2; + } +} + + .commits-compare-switch { - @include btn-default; - @include btn-white; float: left; margin-right: 9px; } @@ -8,7 +77,6 @@ .commit-header { padding: 5px 10px; background-color: $gray-light; - border-top: 1px solid $gray-darker; border-bottom: 1px solid $gray-darker; font-size: 14px; @@ -18,8 +86,6 @@ } .commit-row-title { - line-height: 1.35; - .notes_count { float: right; margin-right: 10px; @@ -30,15 +96,14 @@ } .commit-row-message { - color: $gl-dark-link-color; + color: $gl-text-color; } - } .text-expander { display: inline-block; background: $gray-light; - color: $gl-gray-light; + color: $gl-text-color-secondary; padding: 0 5px; cursor: pointer; border: 1px solid $border-gray-dark; @@ -54,9 +119,8 @@ .commit-actions { @media (min-width: $screen-sm-min) { - float: right; - margin-left: $gl-padding; - margin-top: 2px; + width: 300px; + text-align: right; font-size: 0; } @@ -86,35 +150,13 @@ .commit, .generic_commit_status { - padding: 10px 0; - position: relative; - - @media (min-width: $screen-sm-min) { - padding-left: 46px; - } - - &:not(:last-child) { - border-bottom: 1px solid $gray-darker; - } a, button { - color: $gl-dark-link-color; + color: $gl-text-color; vertical-align: baseline; } - .avatar { - margin-left: -46px; - } - - .item-title { - display: inline-block; - - @media (min-width: $screen-sm-min) { - max-width: 70%; - } - } - .commit-row-description { font-size: 14px; border-left: 1px solid $white-normal; @@ -134,20 +176,7 @@ } a { - color: $gl-dark-link-color; - } - } - - .commit-row-info { - color: $gl-gray; - line-height: 1.35; - - a { - color: $gl-gray; - } - - .avatar { - margin-right: 8px; + color: $gl-text-color; } } @@ -164,7 +193,7 @@ } .branch-commit { - color: $gl-gray; + color: $gl-text-color; .commit-icon { text-align: center; @@ -174,7 +203,7 @@ height: 14px; width: 14px; vertical-align: middle; - fill: $gl-gray-light; + fill: $gl-text-color-secondary; } } @@ -183,62 +212,6 @@ } .commit-row-message { - color: $gl-gray; - } -} - -.divergence-graph { - padding: 12px 12px 0 0; - float: right; - - .graph-side { - position: relative; - width: 80px; - height: 22px; - padding: 5px 0 13px; - float: left; - - .bar { - position: absolute; - height: 4px; - background-color: $divergence-graph-bar-bg; - } - - .bar-behind { - right: 0; - border-radius: 3px 0 0 3px; - } - - .bar-ahead { - left: 0; - border-radius: 0 3px 3px 0; - } - - .count { - padding-top: 6px; - padding-bottom: 0; - font-size: 12px; - color: $gl-title-color; - display: block; - } - - .count-behind { - padding-right: 4px; - text-align: right; - } - - .count-ahead { - padding-left: 4px; - text-align: left; - } - } - - .graph-separator { - position: relative; - width: 1px; - height: 18px; - margin: 5px 0 0; - float: left; - background-color: $divergence-graph-separator-bg; + color: $gl-text-color; } } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 9ce261eafef..cda069e6c0e 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -20,6 +20,10 @@ .fa { color: $cycle-analytics-light-gray; + + &:hover { + color: $gl-text-color; + } } .stage-header { @@ -111,14 +115,14 @@ line-height: 19px; font-size: 14px; font-weight: 600; - color: $gl-title-color; + color: $gl-text-color; } &.text { color: $layout-link-gray; &.value-col { - color: $gl-title-color; + color: $gl-text-color; } } } @@ -260,7 +264,7 @@ .stage-empty, .not-available { - color: $gl-text-color-light; + color: $gl-text-color-secondary; } } } @@ -327,7 +331,7 @@ @include text-overflow(); a { - color: $gl-dark-link-color; + color: $gl-text-color; } } } @@ -355,7 +359,7 @@ .issue-link, .commit-author-link, .issue-author-link { - color: $gl-dark-link-color; + color: $gl-text-color; } // Custom CSS for components @@ -396,11 +400,11 @@ } .item-build-name { - color: $gl-title-color; + color: $gl-text-color; } .pipeline-id { - color: $gl-title-color; + color: $gl-text-color; padding: 0 3px 0 0; } @@ -423,7 +427,7 @@ } .fa { - color: $gl-text-color-light; + color: $gl-text-color-secondary; font-size: $code_font_size; } } @@ -435,7 +439,7 @@ width: 75%; margin: 0 auto; padding-top: 130px; - color: $gl-text-color-light; + color: $gl-text-color-secondary; h4 { color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 9b28df1afc5..46fd19c93f9 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -1,15 +1,15 @@ .detail-page-header { padding: $gl-padding-top 0; border-bottom: 1px solid $border-color; - color: $gl-text-color-dark; + color: $gl-text-color; line-height: 34px; .author { - color: $gl-text-color-dark; + color: $gl-text-color; } .identifier { - color: $gl-text-color-dark; + color: $gl-text-color; } .issue_created_ago, @@ -22,7 +22,7 @@ .title { margin: 0 0 16px; font-size: 2em; - color: $gl-gray-dark; + color: $gl-text-color; padding: 0 0 0.3em; border-bottom: 1px solid $white-dark; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index f30795fd2c2..96ba7c40634 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -14,7 +14,7 @@ background: $gray-light; border-bottom: 1px solid $border-color; padding: 10px 16px; - color: $gl-diff-text-color; + color: $gl-text-color; z-index: 10; border-radius: 3px 3px 0 0; @@ -50,7 +50,7 @@ overflow: auto; overflow-y: hidden; background: $white-light; - color: $gl-title-color; + color: $gl-text-color; border-radius: 0 0 3px 3px; .unfold { @@ -380,7 +380,7 @@ } cursor: default; - color: $gl-title-color; + color: $gl-text-color; } &.disabled { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 5517dc5dcbd..778ef01430e 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -72,25 +72,25 @@ .external-url, .dropdown-new { - color: $gl-gray-light; + color: $gl-text-color-secondary; } .dropdown-menu { .fa { margin-right: 6px; - color: $gl-gray-light; + color: $gl-text-color-secondary; } } .build-link, .branch-name { - color: $gl-dark-link-color; + color: $gl-text-color; } .stop-env-link, .external-url { - color: $gl-gray-light; + color: $gl-text-color-secondary; .stop-env-icon { font-size: 14px; @@ -101,7 +101,7 @@ .build-column { .build-link { - color: $gl-dark-link-color; + color: $gl-text-color; } .avatar { diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index 98925c2d0cb..b989d72ce1c 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -21,7 +21,7 @@ } a { - color: $gl-dark-link-color; + color: $gl-text-color; } .avatar { diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 16bff5f1e03..d377526e655 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -13,7 +13,7 @@ .stats { float: right; line-height: $list-text-height; - color: $gl-gray; + color: $gl-text-color; span { margin-right: 15px; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 6b4d1f85564..4ef95d27f4f 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -1,3 +1,48 @@ +// Limit MR description for side-by-side diff view +.fixed-width-container { + max-width: $limited-layout-width - ($gl-padding * 2); + margin-left: auto; + margin-right: auto; +} + +.limit-container-width { + .detail-page-header { + @extend .fixed-width-container; + } + + .issuable-details { + .detail-page-description, + .mr-source-target, + .mr-state-widget, + .merge-manually { + @extend .fixed-width-container; + } + + .merge-request-tabs-holder { + &.affix { + border-bottom: 1px solid $border-color; + + .nav-links { + border: 0; + } + } + } + } + + .merge-request-details { + .emoji-list-container { + @extend .fixed-width-container; + } + } + + .diffs { + .mr-version-controls, + .files-changed { + @extend .fixed-width-container; + } + } +} + .issuable-details { section { .issuable-discussion { @@ -5,6 +50,12 @@ } } + .title { + padding: 0; + margin: 0; + border-bottom: none; + } + // Border around images in issue and MR descriptions. .description img:not(.emoji) { border: 1px solid $white-normal; @@ -97,10 +148,10 @@ } .edit-link { - color: $gl-gray; + color: $gl-text-color; - &:hover { - color: $md-link-color; + &:not([href]):hover { + color: rgba($avatar-border, .2); } } } @@ -133,7 +184,7 @@ } .btn-clipboard:hover { - color: $gl-gray; + color: $gl-text-color; } } @@ -168,7 +219,7 @@ } .no-value { - color: $gl-gray-light; + color: $gl-text-color-secondary; } .sidebar-collapsed-icon { @@ -236,7 +287,7 @@ color: $issuable-sidebar-color; &:hover { - color: $gl-gray; + color: $gl-text-color; } span { @@ -249,16 +300,16 @@ } .avatar:hover { - border-color: $issuable-avatar-hover-border; + border-color: $issuable-sidebar-color; } .btn-clipboard { border: none; - color: $issuable-clipboard-color; + color: $issuable-sidebar-color; &:hover { background: transparent; - color: $gl-gray; + color: $gl-text-color; } } } @@ -277,6 +328,10 @@ &:hover { color: $md-link-color; text-decoration: none; + + .avatar { + border-color: rgba($avatar-border, .2); + } } } @@ -318,6 +373,10 @@ display: inline-block; padding: 5px; + &:nth-of-type(7n) { + padding-right: 0; + } + .author_link { display: block; } @@ -332,7 +391,7 @@ margin-left: 5px; a { - color: $gl-gray-light; + color: $gl-text-color-secondary; } } @@ -414,3 +473,102 @@ } } } + +.time_tracker { + padding-bottom: 0; + border-bottom: 0; + + + .sidebar-collapsed-icon { + + > .stopwatch-svg { + display: inline-block; + } + + svg { + width: 16px; + height: 16px; + fill: $sidebar-collapsed-icon-color; + } + + &:hover svg { + fill: $gl-text-color; + } + } + + .help-button, + .close-help-button { + cursor: pointer; + } + + .compare-meter { + &.within_estimate { + .meter-fill { + background: $gl-primary; + } + } + + &.over_estimate { + .meter-fill { + background: $red-light; + } + + .time-remaining, + .compare-value.spent { + color: $red-light; + } + } + } + + .meter-container { + background: $border-gray-light; + border-radius: 3px; + + .meter-fill { + max-width: 100%; + height: 5px; + border-radius: 3px; + background: $gl-primary; + } + } + + .compare-display-container { + display: flex; + justify-content: space-between; + margin-top: 5px; + + .compare-display { + font-size: 13px; + color: $compare-display-color; + + .compare-value { + color: $gl-text-color; + } + } + } + + .time-tracking-help-state { + background: $white-light; + margin: 16px -20px 0; + padding: 16px 20px; + border-top: 1px solid $border-gray-light; + border-bottom: 1px solid $border-gray-light; + + a:hover { + color: $btn-white-active; + } + } + + .help-state-toggle-enter-active { + transition: all .8s ease; + } + + .help-state-toggle-leave-active { + transition: all .5s ease; + } + + .help-state-toggle-enter, + .help-state-toggle-leave-active { + opacity: 0; + } +} diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index d129eb12a45..21d9b4c54ea 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -117,7 +117,7 @@ .manage-labels-list { .btn-action { - color: $gl-dark-link-color; + color: $gl-text-color; .fa { font-size: 18px; @@ -203,6 +203,10 @@ z-index: 3; border-radius: $label-border-radius; padding: 6px 10px 6px 9px; + + &:hover { + box-shadow: inset 0 0 0 80px $label-remove-border; + } } .btn { diff --git a/app/assets/stylesheets/pages/lint.scss b/app/assets/stylesheets/pages/lint.scss index a7c80dce424..68b6c5ecbd4 100644 --- a/app/assets/stylesheets/pages/lint.scss +++ b/app/assets/stylesheets/pages/lint.scss @@ -9,3 +9,13 @@ color: $lint-correct-color; } } + +.ci-linter { + .ci-editor { + height: 400px; + } + + .ci-template pre { + white-space: pre-wrap; + } +} diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index 712bd3da22f..71ed5b1361a 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -17,14 +17,19 @@ line-height: 1.5; p { - font-size: 18px; + font-size: 16px; color: $login-brand-holder-color; } h1:first-child { font-weight: normal; - margin-bottom: 30px; + margin-bottom: 0.68em; margin-top: 0; + font-size: 34px; + } + + h3 { + font-size: 22px; } img { diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 36ee5d17211..be7193bae04 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -25,7 +25,7 @@ } .form-horizontal { - margin-top: 5px; + margin-top: 20px; @media (min-width: $screen-sm-min) { display: -webkit-flex; @@ -98,6 +98,10 @@ padding-right: 35px; @media (min-width: $screen-sm-min) { + width: 250px; + } + + @media (min-width: $screen-md-min) { width: 350px; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 1b1126695a1..ab68b360f93 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -4,7 +4,7 @@ */ .mr-state-widget { background: $gray-light; - color: $gl-gray; + color: $gl-text-color; border: 1px solid $border-color; border-radius: 2px; @@ -58,7 +58,7 @@ padding-right: 0; a { - color: $gl-gray; + color: $gl-text-color; } } @@ -70,7 +70,7 @@ .ci_widget { border-bottom: 1px solid $well-inner-border; - color: $gl-gray; + color: $gl-text-color; svg { margin-right: 4px; @@ -94,7 +94,7 @@ } .normal { - color: $gl-text-color-dark; + color: $gl-text-color; } .js-deployment-link { @@ -106,7 +106,7 @@ font-weight: 600; font-size: 16px; margin: 5px 0; - color: $gl-gray-dark; + color: $gl-text-color; &.has-conflicts .fa-exclamation-triangle { color: $gl-warning; @@ -190,7 +190,7 @@ } .label-branch { - color: $gl-gray-dark; + color: $gl-text-color; font-family: $monospace_font; font-weight: bold; overflow: hidden; @@ -310,10 +310,6 @@ left: 0; top: 2px; } - - .commit-row-info { - line-height: 20px; - } } .btn-clipboard { @@ -367,7 +363,7 @@ th { background-color: $white-light; - color: $gl-gray-light; + color: $gl-text-color-secondary; } } } @@ -424,10 +420,6 @@ .merge-request-tabs-holder { background-color: $white-light; - .container-limited { - max-width: $limited-layout-width; - } - &.affix { top: 100px; left: 0; @@ -437,10 +429,26 @@ @media (max-width: $screen-xs-max) { right: 0; } + + .merge-request-tabs-container { + padding-left: $gl-padding; + padding-right: $gl-padding; + } } +} - &:not(.affix) .container-fluid { - padding-left: 0; - padding-right: 0; +.limit-container-width { + .merge-request-tabs-container { + max-width: $limited-layout-width; + margin-left: auto; + margin-right: auto; + } +} + +.limit-container-width:not(.container-limited) { + .merge-request-tabs-holder:not(.affix) { + .merge-request-tabs-container { + max-width: $limited-layout-width - ($gl-padding * 2); + } } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index f47ae9c6157..686b64cdd24 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -102,13 +102,17 @@ margin-top: 7px; .issuable-number { - color: $gl-gray-light; + color: $gl-text-color-secondary; margin-right: 5px; } .avatar { float: none; } + + > a:not(:last-of-type) { + margin-right: 5px; + } } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index e54e12be82f..f984b469609 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -27,6 +27,7 @@ .new-note, .note-edit-form { .note-form-actions { + position: relative; margin-top: $gl-padding; } @@ -44,7 +45,7 @@ .note-textarea { display: block; padding: 10px 0; - color: $gl-gray; + color: $gl-text-color; font-family: $regular_font; border: 0; @@ -204,7 +205,7 @@ .comment-toolbar { padding-top: $gl-padding-top; - color: $gl-gray-light; + color: $gl-text-color-secondary; border-top: 1px solid $border-color; } @@ -265,3 +266,18 @@ } } } + +.note-edit-warning.settings-message { + display: none; + padding: 5px 10px; + position: absolute; + left: 127px; + top: 2px; + + @media (max-width: $screen-xs-max) { + position: relative; + top: 0; + left: 0; + margin-bottom: 10px; + } +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 8f15775ee03..da0caa30c26 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -43,7 +43,7 @@ ul.notes { } .system-note-message { - display: inline-block; + display: inline; &::first-letter { text-transform: lowercase; @@ -55,7 +55,7 @@ ul.notes { } p { - display: inline-block; + display: inline; margin: 0; &::first-letter { @@ -195,10 +195,10 @@ ul.notes { } .note-body { - overflow: auto; + overflow-x: auto; + overflow-y: hidden; .note-text { - overflow: auto; word-wrap: break-word; @include md-typography; // Reset ul style types since we're nested inside a ul already @@ -345,7 +345,7 @@ ul.notes { } .author_link { - color: $gl-gray; + color: $gl-text-color; } } @@ -353,6 +353,14 @@ ul.notes { font-size: 14px; } +.note-headline-light { + display: inline; + + @media (max-width: $screen-xs-min) { + display: block; + } +} + .note-headline-light, .discussion-headline-light { color: $notes-light-color; @@ -507,7 +515,6 @@ ul.notes { .line-resolve-all-container { .btn-group { - margin-top: -1px; margin-left: -4px; } @@ -518,8 +525,9 @@ ul.notes { } .line-resolve-all { + vertical-align: middle; display: inline-block; - padding: 5px 10px; + padding: 6px 10px; background-color: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; @@ -527,18 +535,14 @@ ul.notes { &.has-next-btn { border-top-right-radius: 0; border-bottom-right-radius: 0; + border-right: 0; } .line-resolve-btn { - vertical-align: middle; margin-right: 5px; } } -.line-resolve-text { - vertical-align: middle; -} - .line-resolve-btn { display: inline-block; position: relative; @@ -580,13 +584,11 @@ ul.notes { // Merge request notes in diffs .diff-file { - // Diff is side by side .notes_content.parallel .note-header .note-headline-light { display: block; position: relative; } - // Diff is inline .notes_content .note-header .note-headline-light { display: inline-block; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index c370b6247b0..47dfc22d533 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -1,4 +1,9 @@ .pipelines { + .realtime-loading { + font-size: 40px; + text-align: center; + } + .stage { max-width: 90px; width: 90px; @@ -24,6 +29,10 @@ min-width: 1200px; table-layout: fixed; + .label { + margin-bottom: 3px; + } + .pipeline-id { color: $black; } @@ -35,8 +44,8 @@ .pipeline-info, .pipeline-commit, - .pipeline-actions, - .pipeline-stages { + .pipeline-stages, + .pipeline-actions { width: 20%; } } @@ -131,7 +140,7 @@ height: 14px; width: 14px; vertical-align: middle; - fill: $gl-gray-light; + fill: $gl-text-color-secondary; } .fa { @@ -176,13 +185,16 @@ .stage-cell { font-size: 0; + padding: 10px 4px; - svg { - height: 18px; - width: 18px; - position: relative; + > .stage-container > div > button > span > svg, + > .stage-container > button > svg { + height: 22px; + width: 22px; + position: absolute; + top: -1px; + left: -1px; z-index: 2; - vertical-align: middle; overflow: visible; } @@ -195,10 +207,14 @@ white-space: nowrap; } + .tooltip-inner { + padding: 3px 4px; + } + &:not(:last-child) { &::after { content: ''; - width: 8px; + width: 7px; position: absolute; right: -7px; top: 10px; @@ -210,7 +226,7 @@ .duration, .finished-at { - color: $gl-gray-light; + color: $gl-text-color-secondary; margin: 4px 0; .fa { @@ -231,7 +247,7 @@ .btn { margin: 0; - color: $gl-gray-light; + color: $gl-text-color-secondary; } .cancel-retry-btns { @@ -244,10 +260,10 @@ .dropdown-toggle, .dropdown-menu { - color: $gl-gray-light; + color: $gl-text-color-secondary; .fa { - color: $gl-gray-light; + color: $gl-text-color-secondary; font-size: 14px; } @@ -276,12 +292,16 @@ } } } + + .tooltip { + white-space: nowrap; + } } .build-link { a { - color: $gl-dark-link-color; + color: $gl-text-color; } } @@ -337,14 +357,16 @@ padding: $gl-padding; white-space: nowrap; transition: max-height 0.3s, padding 0.3s; + overflow: auto; - ul { + .stage-column-list, + .builds-container > ul { padding: 0; } a { text-decoration: none; - color: $gl-text-color-light; + color: $gl-text-color-secondary; } svg { @@ -468,13 +490,54 @@ width: 186px; margin-bottom: 10px; white-space: normal; - color: $gl-text-color-light; + color: $gl-text-color-secondary; + + // Action Icons in big pipeline-graph nodes + > .ci-action-icon-container .ci-action-icon-wrapper { + i { + color: $border-color; + border-radius: 100%; + border: 1px solid $border-color; + padding: 5px 6px; + font-size: 13px; + background: $white-light; + height: 30px; + width: 30px; + + &::before { + position: relative; + top: 3px; + left: 3px; + } + + &:hover { + color: $gl-text-color; + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-bg; + } + } + + .ci-play-icon { + padding: 5px 5px 5px 7px; + } + } + + > .ci-action-icon-container { + position: absolute; + right: 5px; + top: 5px; + } + + .ci-status-icon svg { + height: 20px; + width: 20px; + } .dropdown-menu-toggle { background-color: transparent; border: none; padding: 0; - color: $gl-text-color-light; + color: $gl-text-color-secondary; &:focus { outline: none; @@ -504,16 +567,6 @@ } } - > .ci-action-icon-container { - position: absolute; - right: 5px; - top: 5px; - } - - .ci-status-icon svg { - height: 20px; - width: 20px; - } .arrow { &::before, @@ -596,29 +649,9 @@ } } } - - .grouped-pipeline-dropdown { - .dropdown-build { - .build-content { - width: 100%; - - &:hover { - background-color: $stage-hover-bg; - color: $gl-text-color; - } - } - - .ci-action-icon-container { - padding: 0; - font-size: 11px; - position: absolute; - top: 1px; - right: 8px; - } - } - } } +// Triggers the dropdown in the big pipeline graph .dropdown-counter-badge { color: $border-color; font-weight: 100; @@ -628,66 +661,6 @@ top: 8px; } -.grouped-pipeline-dropdown { - padding: 0; - width: 191px; - min-width: 191px; - left: auto; - right: -195px; - top: -4px; - box-shadow: 0 1px 5px $black-transparent; - - a { - display: inline-block; - } - - .dropdown-build { - .build-content { - width: 100%; - - &:hover { - background-color: $stage-hover-bg; - color: $gl-text-color; - } - } - - .ci-action-icon-container { - padding: 0; - font-size: 11px; - position: absolute; - margin-top: 3px; - right: 7px; - } - } - - ul { - max-height: 245px; - overflow: auto; - margin: 3px 0; - - li { - margin: 4px 8px 4px 9px; - padding: 0; - line-height: 1.1; - position: relative; - - .ci-action-icon-container:hover { - background-color: transparent; - } - - .ci-status-icon { - position: relative; - top: 2px; - } - } - } -} - -.pipeline-graph .dropdown-build .ci-status-icon svg { - width: 18px; - height: 18px; -} - .ci-status-text { max-width: 110px; white-space: nowrap; @@ -699,177 +672,233 @@ font-weight: 200; } -// Action Icons -.ci-action-icon-container .ci-action-icon-wrapper { - i { - color: $border-color; - border-radius: 100%; - border: 1px solid $border-color; - padding: 5px 6px; - font-size: 13px; - background: $white-light; - height: 30px; - width: 30px; +// Dropdown button in mini pipeline graph +.mini-pipeline-graph-dropdown-toggle { + border-radius: 100px; + background-color: $white-light; + border-width: 1px; + border-style: solid; + width: 22px; + height: 22px; + margin: 0; + padding: 0; + transition: all 0.2s linear; + position: relative; - &::before { - position: relative; - top: 3px; - left: 3px; - } + > .fa.fa-caret-down { + position: absolute; + left: 20px; + top: 5px; + display: inline-block; + visibility: hidden; + opacity: 0; + color: inherit; + font-size: 12px; + transition: visibility 0.1s, opacity 0.1s linear; + } - &:hover { - color: $gl-text-color; - background-color: $stage-hover-bg; - border: 1px solid $stage-hover-bg; + &:active, + &:focus, + &:hover { + outline: none; + width: 35px; + + .fa.fa-caret-down { + visibility: visible; + opacity: 1; } } - .ci-play-icon { - padding: 5px 5px 5px 7px; + // Dropdown button animation in mini pipeline graph + &.ci-status-icon-success { + border-color: $gl-success; + color: $gl-success; + + &:hover, + &:focus, + &:active { + background-color: rgba($gl-success, 0.1); + border-color: $gl-success; + } } -} -.dropdown-build { - color: $gl-text-color-light; + &.ci-status-icon-failed { + border-color: $gl-danger; + color: $gl-danger; - .build-content { - padding: 4px 7px 8px; + &:hover, + &:focus, + &:active { + background-color: rgba($gl-danger, 0.1); + border-color: $gl-danger; + } } - .ci-action-icon-container { - padding: 0; - font-size: 11px; - float: right; - margin-top: 3px; - display: inline-block; - position: relative; + &.ci-status-icon-pending, + &.ci-status-icon-success_with_warnings { + border-color: $gl-warning; + color: $gl-warning; - i { - font-size: 11px; - margin-top: 0; + &:hover, + &:focus, + &:active { + background-color: rgba($gl-warning, 0.1); + border-color: $gl-warning; } } - .ci-action-icon-container { - i { - width: 24px; - height: 24px; + &.ci-status-icon-running { + border-color: $blue-normal; + color: $blue-normal; - &::before { - top: 1px; - left: 1px; - } + &:hover, + &:focus, + &:active { + background-color: rgba($blue-normal, 0.1); + border-color: $blue-normal; } } - .stage { - max-width: 100px; - width: 100px; - } + &.ci-status-icon-canceled, + &.ci-status-icon-disabled, + &.ci-status-icon-not-found, + &.ci-status-icon-manual { + border-color: $gl-text-color; + color: $gl-text-color; - .ci-status-icon svg { - height: 18px; - width: 18px; + &:hover, + &:focus, + &:active { + background-color: rgba($gl-text-color, 0.1); + border-color: $gl-text-color; + } } - .ci-status-text { - max-width: 95px; + &.ci-status-icon-created, + &.ci-status-icon-skipped { + border-color: $gray-darkest; + color: $gray-darkest; + + &:hover, + &:focus, + &:active { + background-color: rgba($gray-darkest, 0.1); + border-color: $gray-darkest; + } } } -/** - * Builds dropdown in mini pipeline - */ -.mini-pipeline-graph { - .builds-dropdown { - background-color: transparent; - padding: 0; - color: $gl-text-color-light; - border: none; - margin: 0; - - &:focus, - &:hover { - outline: none; - margin-right: -8px; +// dropdown content for big and mini pipeline +.big-pipeline-graph-dropdown-menu, +.mini-pipeline-graph-dropdown-menu { + width: 195px; + max-width: 195px; - .ci-status-icon { - width: 32px; - padding: 0 8px 0 0; - transition: width 0.1s cubic-bezier(0.25, 0, 1, 1); + li { + padding: 2px 3px; + } - + .dropdown-caret { - visibility: visible; - opacity: 1; - } - } - } + .scrollable-menu { + max-height: 245px; + overflow: auto; + } - &:focus, - &:active { - .ci-status-icon-success { - background-color: rgba($gl-success, .1); - } + // Loading icon + .builds-dropdown-loading { + margin: 0 auto; + width: 20px; + } + + // Action icon on the right + a.ci-action-icon-wrapper { + color: $action-icon-color; + border: 1px solid $action-icon-color; + border-radius: 20px; + width: 22px; + height: 22px; + padding: 2px 0 0 5px; + cursor: pointer; + float: right; + margin: -26px 9px 0 0; + font-size: 12px; + background-color: $white-light; - .ci-status-icon-failed { - background-color: rgba($gl-danger, .1); - } + &:hover, + &:focus { + text-decoration: none; + color: $gl-text-color; + background-color: $stage-hover-bg; + border: 1px solid transparent; + } + } - .ci-status-icon-pending, - .ci-status-icon-success_with_warnings { - background-color: rgba($gl-warning, .1); - } + // link to the build + .mini-pipeline-graph-dropdown-item { + padding: 3px 7px 4px; + clear: both; + font-weight: normal; + line-height: 1.428571429; + white-space: nowrap; + margin: 0 5px; + border-radius: 3px; - .ci-status-icon-running { - background-color: rgba($blue-normal, .1); - } + // build name + .ci-build-text { + font-weight: 200; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 90px; + color: $gl-text-color-secondary; + margin-left: 2px; + display: inline-block; + top: 1px; + vertical-align: text-bottom; + position: relative; + } - .ci-status-icon-canceled, - .ci-status-icon-disabled, - .ci-status-icon-not-found { - background-color: rgba($gl-gray, .1); - } + // status icon on the left + .ci-status-icon { + top: 3px; + position: relative; - .ci-status-icon-created, - .ci-status-icon-skipped { - background-color: rgba($gray-darkest, .1); + > svg { + overflow: visible; + width: 18px; + height: 18px; } } - .mini-pipeline-graph-icon-container { - .dropdown-caret { - font-size: 11px; - position: absolute; - top: 6px; - left: 20px; - margin-right: -6px; - z-index: 2; - visibility: hidden; - opacity: 0; - transition: visibility 0.1s, opacity 0.1s linear; - } + &:hover, + &:focus { + outline: none; + text-decoration: none; + color: $gl-text-color; + background-color: $stage-hover-bg; } } +} - .dropdown-build .build-content { - padding: 3px 7px 7px; - } - - .builds-dropdown-loading { - margin: 10px auto; - width: 18px; - } - - .grouped-pipeline-dropdown { - right: -172px; - top: 23px; - min-height: 50px; +// Dropdown in the big pipeline graph +.big-pipeline-graph-dropdown-menu { + width: 195px; + min-width: 195px; + left: auto; + right: -195px; + top: -4px; + box-shadow: 0 1px 5px $black-transparent; - a { - color: $gl-text-color-light; + .mini-pipeline-graph-dropdown-item { + .ci-status-icon { + top: -1px; } } +} +/** + * Top arrow in the dropdown in the mini pipeline graph + */ +.mini-pipeline-graph-dropdown-menu { .arrow-up { &::before, &::after { @@ -898,31 +927,8 @@ } /** - * Icons in mini pipeline graph + * Terminal */ -.mini-pipeline-graph-icon-container .ci-status-icon { - display: inline-block; - border: 1px solid; - border-radius: 22px; - margin-right: 1px; - width: 22px; - height: 22px; - position: relative; - z-index: 2; - transition: all 0.1s cubic-bezier(0.25, 0, 1, 1); - - svg { - top: -1px; - left: -1px; - } -} - -.stage-cell .mini-pipeline-graph-icon-container .ci-status-icon svg { - width: 22px; - height: 22px; -} - - .terminal-icon { margin-left: 3px; } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 8b1976bd925..722b3006f7c 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -216,8 +216,8 @@ } } -.user-profile { +.user-profile { .cover-controls a { margin-left: 5px; } @@ -231,8 +231,11 @@ } } - @media (max-width: $screen-xs-max) { + .user-profile-nav { + font-size: 0; + } + @media (max-width: $screen-xs-max) { .cover-block { padding-top: 20px; } @@ -253,6 +256,12 @@ } } } + + .user-profile-nav { + a { + margin-right: 0; + } + } } } @@ -271,4 +280,4 @@ table.u2f-registrations { .scopes-list { padding-left: 18px; } -}
\ No newline at end of file +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index b99be02ab0c..8b59c20cb65 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -9,24 +9,24 @@ .new_project, .edit-project { - fieldset { - - &.features { + .sharing-and-permissions { + .header { + padding-top: $gl-vert-padding; + } - .label-light { - margin-bottom: 0; - } + .label-light { + margin-bottom: 0; + } - .help-block { - margin-top: 0; - } + .help-block { + margin-top: 0; } .form-group { margin-bottom: 5px; } - &> .form-group { + & > .form-group { padding-left: 0; } } @@ -73,7 +73,7 @@ border: 1px solid $border-color; } - &+ .select2 a { + & + .select2 a { border-top-left-radius: 0; border-bottom-left-radius: 0; } @@ -292,7 +292,7 @@ .option-title { font-weight: normal; display: inline-block; - color: $gl-gray-dark; + color: $gl-text-color; } .option-descr { @@ -331,7 +331,7 @@ a.deploy-project-label { padding: 5px; margin-right: 5px; - color: $gl-gray; + color: $gl-text-color; background-color: $row-hover; &:hover { @@ -372,7 +372,7 @@ a.deploy-project-label { } a { - color: $gl-dark-link-color; + color: $gl-text-color; } .dropdown-menu { @@ -426,7 +426,7 @@ a.deploy-project-label { width: 100%; height: 100%; padding-top: $gl-padding; - color: $gl-gray; + color: $gl-text-color; .caption { min-height: 30px; @@ -523,7 +523,7 @@ a.deploy-project-label { &:hover, &:focus { - color: darken($notes-light-color, 15%); + color: $gl-text-color; } } @@ -552,7 +552,7 @@ pre.light-well { margin: 0 7px 7px; h5 { - color: $gl-text-color-dark; + color: $gl-text-color; } .light-well { @@ -587,11 +587,21 @@ pre.light-well { .project-full-name { @include str-truncated; + + @media (max-width: $screen-xs-max) { + max-width: 50%; + } } .controls { line-height: $list-text-height; + .badge { + @media (max-width: $screen-xs-max) { + display: none; + } + } + a:hover { text-decoration: none; } @@ -605,6 +615,12 @@ pre.light-well { top: 2px; } } + + .description p { + @media (max-width: $screen-xs-max) { + max-width: 50%; + } + } } .bottom { @@ -618,7 +634,6 @@ pre.light-well { margin: 0; } - .activity-filter-block { .controls { padding-bottom: 7px; @@ -663,7 +678,7 @@ pre.light-well { } .commit-row-message { - color: $gl-gray; + color: $gl-text-color; } .commit_short_id { @@ -751,7 +766,7 @@ pre.light-well { .protected-branches-list { a { - color: $gl-gray; + color: $gl-text-color; &:hover { color: $gl-link-color; @@ -811,7 +826,31 @@ pre.light-well { .compare-form-group { .dropdown-menu { - width: 300px; + width: 100%; + + @media (min-width: $screen-sm-min) { + width: 300px; + } + } + + + .compare-ellipsis { + width: 100%; + vertical-align: middle; + text-align: center; + margin-top: -20px; + + @media (min-width: $screen-sm-min) { + margin-top: 0; + width: auto; + } + } + + .inline-input-group { + width: 100%; + + @media (min-width: $screen-sm-min) { + width: 250px; + } } } @@ -866,10 +905,18 @@ pre.light-well { } } -.project-feature-nested { +.project-feature { + padding-top: 10px; + @media (min-width: $screen-sm-min) { padding-left: 45px; } + + &.nested { + @media (min-width: $screen-sm-min) { + padding-left: 90px; + } + } } .project-repo-select { @@ -882,8 +929,32 @@ pre.light-well { .variables-table { table-layout: fixed; + &.table-responsive { + border: none; + } + .variable-key { - width: 30%; + width: 300px; + max-width: 300px; + overflow: hidden; + word-wrap: break-word; + + // override bootstrap + white-space: normal!important; + + @media (max-width: $screen-sm-max) { + width: 150px; + max-width: 150px; + } + } + + .variable-value { + @media(max-width: $screen-xs-max) { + width: 150px; + max-width: 150px; + overflow: hidden; + word-wrap: break-word; + } } } diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index cedd4cb2987..88ea92c5afb 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -14,6 +14,19 @@ } } +.search form:hover, +.file-finder-input:hover, +.issuable-search-form:hover, +.search-text-input:hover, +.form-control:hover { + border-color: lighten($dropdown-input-focus-border, 20%); + box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); +} + +input[type="checkbox"]:hover { + box-shadow: 0 0 2px 2px lighten($search-input-focus-shadow-color, 20%), 0 0 0 1px lighten($search-input-focus-shadow-color, 20%); +} + .search { margin-right: 10px; margin-left: 10px; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index ddee2c95247..a28a87ed4f8 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -1,5 +1,5 @@ .settings-list-icon { - color: $gl-gray-light; + color: $gl-text-color-secondary; font-size: $settings-icon-size; line-height: 42px; } diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss index 4acd17360c1..6f31d4ed789 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -19,7 +19,8 @@ overflow: visible; } - &.ci-failed { + &.ci-failed, + &.ci-failed_with_warnings { color: $gl-danger; border-color: $gl-danger; @@ -61,15 +62,15 @@ &.ci-canceled, &.ci-disabled { - color: $gl-gray; - border-color: $gl-gray; + color: $gl-text-color; + border-color: $gl-text-color; &:not(span):hover { - background-color: rgba($gl-gray, .07); + background-color: rgba($gl-text-color, .07); } svg { - fill: $gl-gray; + fill: $gl-text-color; } } @@ -101,15 +102,15 @@ &.ci-created, &.ci-skipped { - color: $gl-gray-light; - border-color: $gl-gray-light; + color: $gl-text-color-secondary; + border-color: $gl-text-color-secondary; &:not(span):hover { - background-color: rgba($gl-gray-light, .07); + background-color: rgba($gl-text-color-secondary, .07); } svg { - fill: $gl-gray-light; + fill: $gl-text-color-secondary; } } @@ -135,3 +136,9 @@ left: 5px; } } + +.ci-status-link { + svg { + overflow: visible; + } +} diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 508b30f3947..01675acc62e 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -90,7 +90,7 @@ } p { - color: $gl-text-color-dark; + color: $gl-text-color; } } diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index cad4e149845..948921efc0b 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -32,6 +32,10 @@ .last-commit { @include str-truncated(506px); + .fa-angle-right { + margin-left: 5px; + } + @media (min-width: $screen-md-min) and (max-width: $screen-md-max) { @include str-truncated(450px); } @@ -78,7 +82,7 @@ i, a { - color: $gl-dark-link-color; + color: $gl-text-color; } img { @@ -104,21 +108,21 @@ padding-right: 8px; .commit-author-name { - color: $gl-gray; + color: $gl-text-color; } } .tree-time-ago { min-width: 135px; - color: $gl-gray-light; + color: $gl-text-color-secondary; } .tree-commit { max-width: 320px; - color: $gl-gray-light; + color: $gl-text-color-secondary; .tree-commit-link { - color: $gl-gray-light; + color: $gl-text-color-secondary; &:hover { text-decoration: underline; @@ -134,21 +138,18 @@ .blob-commit-info { list-style: none; - padding: $gl-padding; background: $gray-light; + padding: 6px 0; border: 1px solid $border-color; border-bottom: none; margin: 0; - .commit { - padding-top: 0; - padding-bottom: 0; + .table-list-cell { + border-bottom: none; + } - .commit-row-title { - .commit-row-message { - font-weight: normal; - } - } + .commit-actions { + width: 200px; } } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 480cb2b9f0d..9bc47bbe173 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -29,7 +29,7 @@ .wiki-last-edit-by { display: block; - color: $gl-gray-light; + color: $gl-text-color-secondary; strong { color: $gl-text-color; @@ -38,7 +38,7 @@ .light { font-weight: normal; - color: $gl-gray-light; + color: $gl-text-color-secondary; } .git-access-header { diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index c2bb8464824..543d5eac504 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -5,7 +5,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end def update - if @application_setting.update_attributes(application_setting_params) + successful = ApplicationSettings::UpdateService + .new(@application_setting, current_user, application_setting_params) + .execute + + if successful redirect_to admin_application_settings_path, notice: 'Application settings saved successfully' else @@ -67,69 +71,78 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] params.require(:application_setting).permit( - :default_projects_limit, - :default_branch_protection, - :signup_enabled, - :signin_enabled, - :require_two_factor_authentication, - :two_factor_grace_period, - :gravatar_enabled, - :sign_in_text, - :after_sign_up_text, - :help_page_text, - :home_page_url, + application_setting_params_ce + ) + end + + def application_setting_params_ce + [ + :admin_notification_email, :after_sign_out_path, - :max_attachment_size, - :session_expire_delay, + :after_sign_up_text, + :akismet_api_key, + :akismet_enabled, + :container_registry_token_expire_delay, + :default_branch_protection, + :default_group_visibility, :default_project_visibility, + :default_projects_limit, :default_snippet_visibility, - :default_group_visibility, - :domain_whitelist_raw, :domain_blacklist_enabled, - :domain_blacklist_raw, :domain_blacklist_file, - :version_check_enabled, - :admin_notification_email, - :user_oauth_applications, - :user_default_external, - :shared_runners_enabled, - :shared_runners_text, + :domain_blacklist_raw, + :domain_whitelist_raw, + :email_author_in_body, + :enabled_git_access_protocol, + :gravatar_enabled, + :help_page_text, + :home_page_url, + :housekeeping_bitmaps_enabled, + :housekeeping_enabled, + :housekeeping_full_repack_period, + :housekeeping_gc_period, + :housekeeping_incremental_repack_period, + :html_emails_enabled, + :koding_enabled, + :koding_url, + :plantuml_enabled, + :plantuml_url, :max_artifacts_size, + :max_attachment_size, :metrics_enabled, :metrics_host, - :metrics_port, - :metrics_pool_size, - :metrics_timeout, :metrics_method_call_threshold, + :metrics_packet_size, + :metrics_pool_size, + :metrics_port, :metrics_sample_interval, + :metrics_timeout, :recaptcha_enabled, - :recaptcha_site_key, :recaptcha_private_key, - :sentry_enabled, - :sentry_dsn, - :akismet_enabled, - :akismet_api_key, - :koding_enabled, - :koding_url, - :email_author_in_body, - :html_emails_enabled, + :recaptcha_site_key, :repository_checks_enabled, - :metrics_packet_size, + :require_two_factor_authentication, + :session_expire_delay, + :sign_in_text, + :signin_enabled, + :signup_enabled, + :sentry_dsn, + :sentry_enabled, :send_user_confirmation_email, - :container_registry_token_expire_delay, - :enabled_git_access_protocol, + :shared_runners_enabled, + :shared_runners_text, :sidekiq_throttling_enabled, :sidekiq_throttling_factor, - :housekeeping_enabled, - :housekeeping_bitmaps_enabled, - :housekeeping_incremental_repack_period, - :housekeeping_full_repack_period, - :housekeeping_gc_period, + :two_factor_grace_period, + :user_default_external, + :user_oauth_applications, + :version_check_enabled, + + disabled_oauth_sign_in_sources: [], + import_sources: [], repository_storages: [], restricted_visibility_levels: [], - import_sources: [], - disabled_oauth_sign_in_sources: [], sidekiq_throttling_queues: [] - ) + ] end end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index add1c819adf..b7722a1d15d 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -61,7 +61,11 @@ class Admin::GroupsController < Admin::ApplicationController end def group_params - params.require(:group).permit( + params.require(:group).permit(group_params_ce) + end + + def group_params_ce + [ :avatar, :description, :lfs_enabled, @@ -69,6 +73,6 @@ class Admin::GroupsController < Admin::ApplicationController :path, :request_access_enabled, :visibility_level - ) + ] end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index df9039b16b2..aa0f8d434dc 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -161,15 +161,6 @@ class Admin::UsersController < Admin::ApplicationController @user ||= User.find_by!(username: params[:id]) end - def user_params - params.require(:user).permit( - :email, :remember_me, :bio, :name, :username, - :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password, - :extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password, - :projects_limit, :can_create_group, :admin, :key_id, :external - ) - end - def redirect_back_or_admin_user(options = {}) redirect_back_or_default(default: default_route, options: options) end @@ -177,4 +168,36 @@ class Admin::UsersController < Admin::ApplicationController def default_route [:admin, @user] end + + def user_params + params.require(:user).permit(user_params_ce) + end + + def user_params_ce + [ + :admin, + :avatar, + :bio, + :can_create_group, + :color_scheme_id, + :email, + :extern_uid, + :external, + :force_random_password, + :hide_no_password, + :hide_no_ssh_key, + :key_id, + :linkedin, + :name, + :password_expires_at, + :projects_limit, + :provider, + :remember_me, + :skype, + :theme_id, + :twitter, + :username, + :website_url + ] + end end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 5f13353baa1..d7a45bacd35 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -18,15 +18,14 @@ class AutocompleteController < ApplicationController if params[:search].blank? # Include current user if available to filter by "Me" if params[:current_user].present? && current_user - @users = [*@users, current_user] + @users = @users.where.not(id: current_user.id) + @users = [current_user, *@users] end if params[:author_id].present? author = User.find_by_id(params[:author_id]) - @users = [author, *@users] if author + @users = [author, *@users].uniq if author end - - @users.uniq! end render json: @users, only: [:name, :username, :id], methods: [:avatar_url] diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 2aaf8f2b451..52e06f4945a 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -1,6 +1,10 @@ module CycleAnalyticsParams extend ActiveSupport::Concern + def options(params) + @options ||= { from: start_date(params), current_user: current_user } + end + def start_date(params) params[:start_date] == '30' ? 30.days.ago : 90.days.ago end diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb deleted file mode 100644 index 5c503c5b698..00000000000 --- a/app/controllers/concerns/global_milestones.rb +++ /dev/null @@ -1,20 +0,0 @@ -module GlobalMilestones - extend ActiveSupport::Concern - - def milestones - epoch = DateTime.parse('1970-01-01') - @milestones = MilestonesFinder.new.execute(@projects, params) - @milestones = GlobalMilestone.build_collection(@milestones) - @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date } - end - - def milestone - milestones = Milestone.of_projects(@projects).where(title: params[:title]) - - if milestones.present? - @milestone = GlobalMilestone.new(params[:title], milestones) - else - render_404 - end - end -end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index 549a8526715..d7f5a4e4682 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -1,31 +1,72 @@ module ServiceParams extend ActiveSupport::Concern - ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain, - :room, :recipients, :project_url, :webhook, - :user_key, :device, :priority, :sound, :bamboo_url, :username, :password, - :build_key, :server, :teamcity_url, :drone_url, :build_type, - :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, - :colorize_messages, :channels, - # We're using `issues_events` and `merge_requests_events` - # in the view so we still need to explicitly state them - # here. `Service#event_names` would only give - # `issue_events` and `merge_request_events` (singular!) - # See app/helpers/services_helper.rb for how we - # make those event names plural as special case. - :issues_events, :confidential_issues_events, :merge_requests_events, - :notify_only_broken_builds, :notify_only_broken_pipelines, - :add_pusher, :send_from_committer_email, :disable_diffs, - :external_wiki_url, :notify, :color, - :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, - :jira_issue_transition_id, :url, :project_key, :ca_pem, :namespace] + ALLOWED_PARAMS_CE = [ + :active, + :add_pusher, + :api_key, + :api_url, + :api_version, + :bamboo_url, + :build_key, + :build_type, + :ca_pem, + :channel, + :channels, + :color, + :colorize_messages, + :confidential_issues_events, + :default_irc_uri, + :description, + :device, + :disable_diffs, + :drone_url, + :enable_ssl_verification, + :external_wiki_url, + # We're using `issues_events` and `merge_requests_events` + # in the view so we still need to explicitly state them + # here. `Service#event_names` would only give + # `issue_events` and `merge_request_events` (singular!) + # See app/helpers/services_helper.rb for how we + # make those event names plural as special case. + :issues_events, + :issues_url, + :jira_issue_transition_id, + :merge_requests_events, + :namespace, + :new_issue_url, + :notify, + :notify_only_broken_builds, + :notify_only_broken_pipelines, + :password, + :priority, + :project_key, + :project_url, + :recipients, + :restrict_to_branch, + :room, + :send_from_committer_email, + :server, + :server_host, + :server_port, + :sound, + :subdomain, + :teamcity_url, + :title, + :token, + :type, + :url, + :user_key, + :username, + :webhook + ] # Parameters to ignore if no value is specified FILTER_BLANK_PARAMS = [:password] def service_params dynamic_params = @service.event_channel_names + @service.event_names - service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params) + service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params) if service_params[:service].is_a?(Hash) FILTER_BLANK_PARAMS.each do |param| diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 3da44b9b888..306afb65f10 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -14,12 +14,8 @@ class ConfirmationsController < Devise::ConfirmationsController if signed_in?(resource_name) after_sign_in_path_for(resource) else - sign_in(resource) - if signed_in?(resource_name) - after_sign_in_path_for(resource) - else - new_session_path(resource_name) - end + flash[:notice] += " Please sign in." + new_session_path(resource_name) end end end diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index fa9c6c054f0..7f506db583f 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -1,6 +1,4 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController - include GlobalMilestones - before_action :projects before_action :milestone, only: [:show] @@ -17,4 +15,15 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController def show end + + private + + def milestones + @milestones = DashboardMilestone.build_collection(@projects, params) + end + + def milestone + @milestone = DashboardMilestone.build(@projects, params[:title]) + render_404 unless @milestone + end end diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index a62c6211372..26e17a7553e 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -22,6 +22,7 @@ class Explore::ProjectsController < Explore::ApplicationController def trending @projects = filter_projects(Project.trending) + @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]) respond_to do |format| diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 24ec4eec3f2..0d872c86c8a 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -1,6 +1,4 @@ class Groups::MilestonesController < Groups::ApplicationController - include GlobalMilestones - before_action :group_projects before_action :milestone, only: [:show, :update] before_action :authorize_admin_milestones!, only: [:new, :create, :update] @@ -73,4 +71,13 @@ class Groups::MilestonesController < Groups::ApplicationController def milestone_path(title) group_milestone_path(@group, title.to_slug.to_s, title: title) end + + def milestones + @milestones = GroupMilestone.build_collection(@group, @projects, params) + end + + def milestone + @milestone = GroupMilestone.build(@group, @projects, params[:title]) + render_404 unless @milestone + end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b61f4e9a2db..f81237db991 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -125,7 +125,11 @@ class GroupsController < Groups::ApplicationController end def group_params - params.require(:group).permit( + params.require(:group).permit(group_params_ce) + end + + def group_params_ce + [ :avatar, :description, :lfs_enabled, @@ -135,7 +139,7 @@ class GroupsController < Groups::ApplicationController :request_access_enabled, :share_with_group_lock, :visibility_level - ) + ] end def load_events diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index fbe391fc58c..886934a3f67 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -94,7 +94,7 @@ class Projects::BuildsController < Projects::ApplicationController private def build - @build ||= project.builds.find_by!(id: params[:id]) + @build ||= project.builds.find_by!(id: params[:id]).present(current_user: current_user) end def build_path(build) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 791ed88db30..bfc59bcc862 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -12,7 +12,6 @@ class Projects::CommitController < Projects::ApplicationController before_action :authorize_read_pipeline!, only: [:pipelines] before_action :commit before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines] - before_action :define_status_vars, only: [:show, :pipelines] before_action :define_note_vars, only: [:show, :diff_for_path] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] @@ -106,10 +105,6 @@ class Projects::CommitController < Projects::ApplicationController } end - def define_status_vars - @ci_pipelines = project.pipelines.where(sha: commit.sha) - end - def assign_change_commit_vars(mr_source_branch) @commit = project.commit(params[:id]) @target_branch = params[:target_branch] diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index ec02fc15d35..d32966645c8 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -25,8 +25,17 @@ class Projects::CompareController < Projects::ApplicationController end def create - redirect_to namespace_project_compare_path(@project.namespace, @project, + if params[:from].blank? || params[:to].blank? + flash[:alert] = "You must select from and to branches" + from_to_vars = { + from: params[:from].presence, + to: params[:to].presence + } + redirect_to namespace_project_compare_index_path(@project.namespace, @project, from_to_vars) + else + redirect_to namespace_project_compare_path(@project.namespace, @project, params[:from], params[:to]) + end end private diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index 13b3eec761f..b69d46f2c41 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -9,56 +9,52 @@ module Projects before_action :authorize_read_merge_request!, only: [:code, :review] def issue - render_events(events.issue_events) + render_events(cycle_analytics[:issue].events) end def plan - render_events(events.plan_events) + render_events(cycle_analytics[:plan].events) end def code - render_events(events.code_events) + render_events(cycle_analytics[:code].events) end def test - options[:branch] = events_params[:branch_name] + options(events_params)[:branch] = events_params[:branch_name] - render_events(events.test_events) + render_events(cycle_analytics[:test].events) end def review - render_events(events.review_events) + render_events(cycle_analytics[:review].events) end def staging - render_events(events.staging_events) + render_events(cycle_analytics[:staging].events) end def production - render_events(events.production_events) + render_events(cycle_analytics[:production].events) end private - - def render_events(events_list) + + def render_events(events) respond_to do |format| format.html - format.json { render json: { events: events_list } } + format.json { render json: { events: events } } end end - def events - @events ||= Gitlab::CycleAnalytics::Events.new(project: project, options: options) - end - - def options - @options ||= { from: start_date(events_params), current_user: current_user } + def cycle_analytics + @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params)) end def events_params return {} unless params[:events].present? - params[:events].slice(:start_date, :branch_name) + params[:events].permit(:start_date, :branch_name) end end end diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index ac639ef015b..88ac3ad046b 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -6,11 +6,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController before_action :authorize_read_cycle_analytics! def show - @cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params)) + @cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params)) - stats_values, cycle_analytics_json = generate_cycle_analytics_data - - @cycle_analytics_no_data = stats_values.blank? + @cycle_analytics_no_data = @cycle_analytics.no_stats? respond_to do |format| format.html @@ -23,50 +21,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController def cycle_analytics_params return {} unless params[:cycle_analytics].present? - { start_date: params[:cycle_analytics][:start_date] } + params[:cycle_analytics].permit(:start_date) end - def generate_cycle_analytics_data - stats_values = [] - - cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"], - [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"], - [:code, "Code", "Related Merge Requests", "Time spent coding"], - [:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"], - [:review, "Review", "Relative Merged Requests", "The time taken to review the code"], - [:staging, "Staging", "Relative Deployed Builds", "The time taken in staging"], - [:production, "Production", "Related Issues", "The total time taken from idea to production"]] - - stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)| - value = @cycle_analytics.send(stage_method).presence - - stats_values << value.abs if value - - stats << { - title: stage_text, - description: stage_description, - legend: stage_legend, - value: value && !value.zero? ? distance_of_time_in_words(value) : nil - } - - stats - end - - issues = @cycle_analytics.summary.new_issues - commits = @cycle_analytics.summary.commits - deploys = @cycle_analytics.summary.deploys - - summary = [ - { title: "New Issue".pluralize(issues), value: issues }, - { title: "Commit".pluralize(commits), value: commits }, - { title: "Deploy".pluralize(deploys), value: deploys } - ] - - cycle_analytics_hash = { summary: summary, - stats: stats, - permissions: @cycle_analytics.permissions(user: current_user) + def cycle_analytics_json + { + summary: @cycle_analytics.summary, + stats: @cycle_analytics.stats, + permissions: @cycle_analytics.permissions(user: current_user) } - - [stats_values, cycle_analytics_hash] end end diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 8714349e27f..70845617d3c 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -109,12 +109,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController end def repository + wiki? ? project.wiki.repository : project.repository + end + + def wiki? + return @wiki if defined?(@wiki) + _, suffix = project_id_with_suffix - if suffix == '.wiki.git' - project.wiki.repository - else - project.repository - end + @wiki = suffix == '.wiki.git' end def render_not_found diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 9184dcccac5..278098fcc58 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -84,7 +84,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController end def access - @access ||= Gitlab::GitAccess.new(user, project, 'http', authentication_abilities: authentication_abilities) + @access ||= access_klass.new(user, project, 'http', authentication_abilities: authentication_abilities) end def access_check @@ -102,4 +102,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController access_check.allowed? end + + def access_klass + @access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess + end end diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index 9eaf26a0dbf..66b7bdbd988 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -4,10 +4,7 @@ class Projects::GroupLinksController < Projects::ApplicationController before_action :authorize_admin_project_member!, only: [:update] def index - @group_links = project.project_group_links.all - - @skip_groups = @group_links.pluck(:group_id) - @skip_groups << project.namespace_id unless project.personal? + redirect_to namespace_project_settings_members_path end def create @@ -25,7 +22,7 @@ class Projects::GroupLinksController < Projects::ApplicationController flash[:alert] = 'Please select a group.' end - redirect_to namespace_project_group_links_path(project.namespace, project) + redirect_to namespace_project_settings_members_path(project.namespace, project) end def update @@ -39,7 +36,7 @@ class Projects::GroupLinksController < Projects::ApplicationController respond_to do |format| format.html do - redirect_to namespace_project_group_links_path(project.namespace, project) + redirect_to namespace_project_settings_members_path(project.namespace, project) end format.js { head :ok } end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 0ae8ff98009..b668a9331e7 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -6,21 +6,15 @@ class Projects::HooksController < Projects::ApplicationController layout "project_settings" - def index - @hooks = @project.hooks - @hook = ProjectHook.new - end - def create @hook = @project.hooks.new(hook_params) @hook.save - if @hook.valid? - redirect_to namespace_project_hooks_path(@project.namespace, @project) - else + unless @hook.valid? @hooks = @project.hooks.select(&:persisted?) - render :index + flash[:alert] = @hook.errors.full_messages.join.html_safe end + redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) end def test @@ -44,7 +38,7 @@ class Projects::HooksController < Projects::ApplicationController def destroy hook.destroy - redirect_to namespace_project_hooks_path(@project.namespace, @project) + redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) end private diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 2beb0df8a07..8472ceca329 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -33,6 +33,18 @@ class Projects::IssuesController < Projects::ApplicationController @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute end + @users = [] + + if params[:assignee_id].present? + assignee = User.find_by_id(params[:assignee_id]) + @users.push(assignee) if assignee + end + + if params[:author_id].present? + author = User.find_by_id(params[:author_id]) + @users.push(author) if author + end + respond_to do |format| format.html format.atom { render layout: false } diff --git a/app/controllers/projects/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb index d87dff2a80e..01d99c7df35 100644 --- a/app/controllers/projects/mattermosts_controller.rb +++ b/app/controllers/projects/mattermosts_controller.rb @@ -30,7 +30,7 @@ class Projects::MattermostsController < Projects::ApplicationController def configure_params params.require(:mattermost).permit(:trigger, :team_id).merge( url: service_trigger_url(@service), - icon_url: asset_url('gitlab_logo.png')) + icon_url: asset_url('slash-command-logo.png')) end def teams diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index fc8a289d49d..9ac5bf4b9f8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -98,7 +98,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } unless @start_version - render_404 + @start_sha = @merge_request_diff.head_commit_sha + @start_version = @merge_request_diff end end @@ -346,6 +347,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def merge_widget_refresh + if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged' + @status = :success + elsif merge_request.merge_when_build_succeeds + @status = :merge_when_build_succeeds + end + + render 'merge' + end + def branch_from # This is always source @source_project = @merge_request.nil? ? @project : @merge_request.source_project @@ -408,10 +419,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController else ci_service = @merge_request.source_project.try(:ci_service) status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service - - if ci_service.respond_to?(:commit_coverage) - coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch) - end end response = { diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index b71509f2c9b..c5d93ce25bc 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -23,7 +23,8 @@ class Projects::NotesController < Projects::ApplicationController end def create - @note = Notes::CreateService.new(project, current_user, note_params).execute + create_params = note_params.merge(merge_request_diff_head_sha: params[:merge_request_diff_head_sha]) + @note = Notes::CreateService.new(project, current_user, create_params).execute if @note.is_a?(Note) Banzai::NoteRenderer.render([@note], @project, current_user) diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index cc347922c6a..84451257b98 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -7,11 +7,33 @@ class Projects::PipelinesController < Projects::ApplicationController def index @scope = params[:scope] - @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30) - @pipelines = @pipelines.includes(project: :namespace) + @pipelines = PipelinesFinder + .new(project) + .execute(scope: @scope) + .page(params[:page]) + .per(30) - @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count - @pipelines_count = PipelinesFinder.new(project).execute.count + @running_or_pending_count = PipelinesFinder + .new(project).execute(scope: 'running').count + + @pipelines_count = PipelinesFinder + .new(project).execute.count + + respond_to do |format| + format.html + format.json do + render json: { + pipelines: PipelineSerializer + .new(project: @project, user: @current_user) + .with_pagination(request, response) + .represent(@pipelines), + count: { + all: @pipelines_count, + running_or_pending: @running_or_pending_count + } + } + end + end end def new diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 3aec6f18e27..6e158e685e9 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -6,54 +6,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] def index - @sort = params[:sort].presence || sort_value_name - @group_links = @project.project_group_links - - @project_members = @project.project_members - @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) - - group = @project.group - - if group - # We need `.where.not(user_id: nil)` here otherwise when a group has an - # invitee, it would make the following query return 0 rows since a NULL - # user_id would be present in the subquery - # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values - # FIXME: This whole logic should be moved to a finder! - non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id) - group_members = group.group_members.where.not(user_id: non_null_user_ids) - group_members = group_members.non_invite unless can?(current_user, :admin_group, @group) - end - - if params[:search].present? - user_ids = @project.users.search(params[:search]).select(:id) - @project_members = @project_members.where(user_id: user_ids) - - if group_members - user_ids = group.users.search(params[:search]).select(:id) - group_members = group_members.where(user_id: user_ids) - end - - @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) - end - - wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"] - wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members - - @project_members = Member. - where(wheres.join(' OR ')). - sort(@sort). - page(params[:page]) - - @requesters = AccessRequestsFinder.new(@project).execute(current_user) - - @project_member = @project.project_members.new + sort = params[:sort].presence || sort_value_name + redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort) end def create status = Members::CreateService.new(@project, current_user, params).execute - redirect_url = namespace_project_project_members_path(@project.namespace, @project) + redirect_url = namespace_project_settings_members_path(@project.namespace, @project) if status redirect_to redirect_url, notice: 'Users were successfully added.' @@ -76,14 +36,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController respond_to do |format| format.html do - redirect_to namespace_project_project_members_path(@project.namespace, @project) + redirect_to namespace_project_settings_members_path(@project.namespace, @project) end format.js { head :ok } end end def resend_invite - redirect_path = namespace_project_project_members_path(@project.namespace, @project) + redirect_path = namespace_project_settings_members_path(@project.namespace, @project) @project_member = @project.project_members.find(params[:id]) @@ -106,7 +66,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController return render_404 end - redirect_to(namespace_project_project_members_path(project.namespace, project), + redirect_to(namespace_project_settings_members_path(project.namespace, project), notice: notice) end diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 3602b3d5e58..667f4870c7a 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -32,12 +32,6 @@ class Projects::RefsController < Projects::ApplicationController redirect_to new_path end - format.js do - @ref = params[:ref] - define_tree_vars - tree - render "tree" - end end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 30c2a5d9982..17cb1d5be24 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -9,10 +9,6 @@ class Projects::ServicesController < Projects::ApplicationController layout "project_settings" - def index - @services = @project.find_or_initialize_services - end - def edit end diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb new file mode 100644 index 00000000000..fb2a4837735 --- /dev/null +++ b/app/controllers/projects/settings/integrations_controller.rb @@ -0,0 +1,18 @@ +module Projects + module Settings + class IntegrationsController < Projects::ApplicationController + include ServiceParams + + before_action :authorize_admin_project! + layout "project_settings" + + def show + @hooks = @project.hooks + @hook = ProjectHook.new + + # Services + @services = @project.find_or_initialize_services + end + end + end +end diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb new file mode 100644 index 00000000000..5735e281f66 --- /dev/null +++ b/app/controllers/projects/settings/members_controller.rb @@ -0,0 +1,55 @@ +module Projects + module Settings + class MembersController < Projects::ApplicationController + include SortingHelper + + def show + @sort = params[:sort].presence || sort_value_name + @group_links = @project.project_group_links + + @project_members = @project.project_members + @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project) + + group = @project.group + + # group links + @group_links = @project.project_group_links.all + + @skip_groups = @group_links.pluck(:group_id) + @skip_groups << @project.namespace_id unless @project.personal? + + if group + # We need `.where.not(user_id: nil)` here otherwise when a group has an + # invitee, it would make the following query return 0 rows since a NULL + # user_id would be present in the subquery + # See http://stackoverflow.com/questions/129077/not-in-clause-and-null-values + group_members = MembersFinder.new(@project_members, group).execute(current_user) + end + + if params[:search].present? + user_ids = @project.users.search(params[:search]).select(:id) + @project_members = @project_members.where(user_id: user_ids) + + if group_members + user_ids = group.users.search(params[:search]).select(:id) + group_members = group_members.where(user_id: user_ids) + end + + @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) + end + + wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"] + wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members + + @project_members = Member. + where(wheres.join(' OR ')). + sort(@sort). + page(params[:page]) + + @requesters = AccessRequestsFinder.new(@project).execute(current_user) + + @project_member = @project.project_members.new + end + end + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index d5ee503c44c..444ff837bb3 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -42,19 +42,16 @@ class ProjectsController < Projects::ApplicationController end def update - status = ::Projects::UpdateService.new(@project, current_user, project_params).execute + result = ::Projects::UpdateService.new(@project, current_user, project_params).execute # Refresh the repo in case anything changed - @repository = project.repository + @repository = @project.repository respond_to do |format| - if status + if result[:status] == :success flash[:notice] = "Project '#{@project.name}' was successfully updated." format.html do - redirect_to( - edit_project_path(@project), - notice: "Project '#{@project.name}' was successfully updated." - ) + redirect_to(edit_project_path(@project)) end else format.html { render 'edit' } diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index c45196cc3e9..bf27f3d4d51 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -7,17 +7,17 @@ class RegistrationsController < Devise::RegistrationsController end def create - if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha - # To avoid duplicate form fields on the login page, the registration form - # names fields using `new_user`, but Devise still wants the params in - # `user`. - if params["new_#{resource_name}"].present? && params[resource_name].blank? - params[resource_name] = params.delete(:"new_#{resource_name}") - end + # To avoid duplicate form fields on the login page, the registration form + # names fields using `new_user`, but Devise still wants the params in + # `user`. + if params["new_#{resource_name}"].present? && params[resource_name].blank? + params[resource_name] = params.delete(:"new_#{resource_name}") + end + if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha super else - flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code." + flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.' flash.delete :recaptcha_error render action: 'new' end @@ -57,7 +57,7 @@ class RegistrationsController < Devise::RegistrationsController end def sign_up_params - params.require(:user).permit(:username, :email, :name, :password, :password_confirmation) + params.require(:user).permit(:username, :email, :email_confirmation, :name, :password) end def resource_name diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index b666aa01d6b..6576ebd5235 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -45,6 +45,8 @@ class SearchController < ApplicationController end @search_objects = @search_results.objects(@scope, params[:page]) + + check_single_commit_result end def autocomplete @@ -59,4 +61,16 @@ class SearchController < ApplicationController render json: search_autocomplete_opts(term).to_json end + + private + + def check_single_commit_result + if @search_results.single_commit_result? + only_commit = @search_results.objects('commits').first + query = params[:search].strip.downcase + found_by_commit_sha = Commit.valid_hash?(query) && only_commit.sha.start_with?(query) + + redirect_to namespace_project_commit_path(@project.namespace, @project, only_commit) if found_by_commit_sha + end + end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index b4c14d05eaf..1576fc80a6b 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -165,31 +165,53 @@ class IssuableFinder end end - def assignee? - params[:assignee_id].present? + def assignee_id? + params[:assignee_id].present? && params[:assignee_id] != NONE + end + + def assignee_username? + params[:assignee_username].present? && params[:assignee_username] != NONE + end + + def no_assignee? + # Assignee_id takes precedence over assignee_username + params[:assignee_id] == NONE || params[:assignee_username] == NONE end def assignee return @assignee if defined?(@assignee) @assignee = - if assignee? && params[:assignee_id] != NONE - User.find(params[:assignee_id]) + if assignee_id? + User.find_by(id: params[:assignee_id]) + elsif assignee_username? + User.find_by(username: params[:assignee_username]) else nil end end - def author? - params[:author_id].present? + def author_id? + params[:author_id].present? && params[:author_id] != NONE + end + + def author_username? + params[:author_username].present? && params[:author_username] != NONE + end + + def no_author? + # author_id takes precedence over author_username + params[:author_id] == NONE || params[:author_username] == NONE end def author return @author if defined?(@author) @author = - if author? && params[:author_id] != NONE - User.find(params[:author_id]) + if author_id? + User.find_by(id: params[:author_id]) + elsif author_username? + User.find_by(username: params[:author_username]) else nil end @@ -263,16 +285,24 @@ class IssuableFinder end def by_assignee(items) - if assignee? - items = items.where(assignee_id: assignee.try(:id)) + if assignee + items = items.where(assignee_id: assignee.id) + elsif no_assignee? + items = items.where(assignee_id: nil) + elsif assignee_id? || assignee_username? # assignee not found + items = items.none end items end def by_author(items) - if author? - items = items.where(author_id: author.try(:id)) + if author + items = items.where(author_id: author.id) + elsif no_author? + items = items.where(author_id: nil) + elsif author_id? || author_username? # author not found + items = items.none end items diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb new file mode 100644 index 00000000000..702944404f5 --- /dev/null +++ b/app/finders/members_finder.rb @@ -0,0 +1,13 @@ +class MembersFinder < Projects::ApplicationController + def initialize(project_members, project_group) + @project_members = project_members + @project_group = project_group + end + + def execute(current_user) + non_null_user_ids = @project_members.where.not(user_id: nil).select(:user_id) + group_members = @project_group.group_members.where.not(user_id: non_null_user_ids) + group_members = group_members.non_invite unless can?(current_user, :admin_group, @project_group) + group_members + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index c816b616631..a112928c6de 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -244,7 +244,9 @@ module ApplicationHelper scope: params[:scope], milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], + assignee_username: params[:assignee_username], author_id: params[:author_id], + author_username: params[:author_username], search: params[:search], label_name: params[:label_name] } diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index aa54ee07bdc..2aa0449c46e 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -3,7 +3,7 @@ module CompareHelper from.present? && to.present? && from != to && - project.feature_available?(:merge_requests, current_user) && + can?(current_user, :create_merge_request, project) && project.repository.branch_names.include?(from) && project.repository.branch_names.include?(to) end diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index c35d6611ab0..aed1d7c839f 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -165,4 +165,10 @@ module DiffHelper link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class] end + + def render_overflow_warning?(diff_files) + diffs = @merge_request_diff.presence || diff_files + + diffs.overflow? + end end diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index eb435cc1783..6d365ea9251 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -110,6 +110,28 @@ module GitlabMarkdownHelper end end + # Returns the text necessary to reference `entity` across projects + # + # project - Project to reference + # entity - Object that responds to `to_reference` + # + # Examples: + # + # cross_project_reference(project, project.issues.first) + # # => 'namespace1/project1#123' + # + # cross_project_reference(project, project.merge_requests.first) + # # => 'namespace1/project1!345' + # + # Returns a String + def cross_project_reference(project, entity) + if entity.respond_to?(:to_reference) + entity.to_reference(project, full: true) + else + '' + end + end + private # Return +text+, truncated to +max_chars+ characters, excluding any HTML @@ -158,28 +180,6 @@ module GitlabMarkdownHelper end end - # Returns the text necessary to reference `entity` across projects - # - # project - Project to reference - # entity - Object that responds to `to_reference` - # - # Examples: - # - # cross_project_reference(project, project.issues.first) - # # => 'namespace1/project1#123' - # - # cross_project_reference(project, project.merge_requests.first) - # # => 'namespace1/project1!345' - # - # Returns a String - def cross_project_reference(project, entity) - if entity.respond_to?(:to_reference) - entity.to_reference(project) - else - '' - end - end - def markdown_toolbar_button(options = {}) data = options[:data].merge({ container: "body" }) content_tag :button, diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 99db73c9ee0..2159e4ce21a 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -206,4 +206,13 @@ module GitlabRoutingHelper file_namespace_project_build_artifacts_path(*args) end end + + # Settings + def project_settings_integrations_path(project, *args) + namespace_project_settings_integrations_path(project.namespace, project, *args) + end + + def project_settings_members_path(project, *args) + namespace_project_settings_members_path(project.namespace, project, *args) + end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 77dc9e7d538..926c9703628 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -14,7 +14,7 @@ module GroupsHelper def group_title(group, name = nil, url = nil) full_title = '' - group.parents.each do |parent| + group.ancestors.each do |parent| full_title += link_to(simple_sanitize(parent.name), group_path(parent)) full_title += ' / '.html_safe end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 1c213983a5b..e5bb8b93e76 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -30,6 +30,15 @@ module IssuablesHelper end end + def serialize_issuable(issuable) + case issuable + when Issue + IssueSerializer.new.represent(issuable).to_json + when MergeRequest + MergeRequestSerializer.new.represent(issuable).to_json + end + end + def template_dropdown_tag(issuable, &block) title = selected_template(issuable) || "Choose a template" options = { diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index a8a49e43b05..a2d21b67a77 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -58,13 +58,13 @@ module IssuesHelper end def status_box_class(item) - if item.respond_to?(:expired?) && item.expired? + if item.try(:expired?) 'status-box-expired' - elsif item.respond_to?(:merged?) && item.merged? + elsif item.try(:merged?) 'status-box-merged' elsif item.closed? 'status-box-closed' - elsif item.respond_to?(:upcoming?) && item.upcoming? + elsif item.try(:upcoming?) 'status-box-upcoming' else 'status-box-open' @@ -128,8 +128,10 @@ module IssuesHelper names.to_sentence end - def award_active_class(awards, current_user) - if current_user && awards.find { |a| a.user_id == current_user.id } + def award_state_class(awards, current_user) + if !current_user + "disabled" + elsif current_user && awards.find { |a| a.user_id == current_user.id } "active" else "" diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 20218775659..8c2c4e8833b 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -19,6 +19,14 @@ module MergeRequestsHelper } end + def mr_widget_refresh_url(mr) + if mr && mr.source_project + merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr) + else + '' + end + end + def mr_css_classes(mr) classes = "merge-request" classes << " closed" if mr.closed? diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c6c63918fa5..eb98204285d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -409,4 +409,15 @@ module ProjectsHelper def project_issues(project) IssuesFinder.new(current_user, project_id: project.id).execute end + + def visibility_select_options(project, selected_level) + levels_options_array = Gitlab::VisibilityLevel.values.map do |level| + [ + visibility_level_label(level), + { data: { description: visibility_level_description(level, project) } }, + level + ] + end + options_for_select(levels_options_array, selected_level) + end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index cdb9663877c..6654f6997ce 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -39,7 +39,7 @@ module SearchHelper # Autocomplete results for various settings pages def default_autocomplete [ - { category: "Settings", label: "Profile settings", url: profile_path }, + { category: "Settings", label: "User settings", url: profile_path }, { category: "Settings", label: "SSH Keys", url: profile_keys_path }, { category: "Settings", label: "Dashboard", url: root_path }, { category: "Settings", label: "Admin Section", url: admin_root_path }, @@ -75,7 +75,7 @@ module SearchHelper { category: "Current Project", label: "Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) }, { category: "Current Project", label: "Milestones", url: namespace_project_milestones_path(@project.namespace, @project) }, { category: "Current Project", label: "Snippets", url: namespace_project_snippets_path(@project.namespace, @project) }, - { category: "Current Project", label: "Members", url: namespace_project_project_members_path(@project.namespace, @project) }, + { category: "Current Project", label: "Members", url: namespace_project_settings_members_path(@project.namespace, @project) }, { category: "Current Project", label: "Wiki", url: namespace_project_wikis_path(@project.namespace, @project) }, ] else diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index 9bab140e60a..715e5893a2c 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -1,23 +1,23 @@ module ServicesHelper def service_event_description(event) case event - when "push" + when "push", "push_events" "Event will be triggered by a push to the repository" - when "tag_push" + when "tag_push", "tag_push_events" "Event will be triggered when a new tag is pushed to the repository" - when "note" + when "note", "note_events" "Event will be triggered when someone adds a comment" - when "issue" + when "issue", "issue_events" "Event will be triggered when an issue is created/updated/closed" - when "confidential_issue" + when "confidential_issue", "confidential_issue_events" "Event will be triggered when a confidential issue is created/updated/closed" - when "merge_request" + when "merge_request", "merge_request_events" "Event will be triggered when a merge request is created/updated/merged" - when "build" + when "build", "build_events" "Event will be triggered when a build status changes" - when "wiki_page" + when "wiki_page", "wiki_page_events" "Event will be triggered when a wiki page is created/updated" - when "commit" + when "commit", "commit_events" "Event will be triggered when a commit is created/updated" end end @@ -26,4 +26,6 @@ module ServicesHelper event = event.pluralize if %w[merge_request issue confidential_issue].include?(event) "#{event}_events" end + + extend self end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 74de25acf9d..c568cca9e5e 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -11,9 +11,10 @@ module TodosHelper case todo.action when Todo::ASSIGNED then 'assigned you' when Todo::MENTIONED then 'mentioned you on' - when Todo::BUILD_FAILED then 'The build failed for your' + when Todo::BUILD_FAILED then 'The build failed for' when Todo::MARKED then 'added a todo for' when Todo::APPROVAL_REQUIRED then 'set you as an approver for' + when Todo::UNMERGEABLE then 'Could not merge' end end diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index a674564c4ec..456598b4c28 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -1,7 +1,8 @@ module VersionCheckHelper def version_status_badge if Rails.env.production? && current_application_settings.version_check_enabled - image_tag VersionCheck.new.url + image_url = VersionCheck.new.url + image_tag image_url, class: 'js-version-status-badge' end end end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 0d20c9092c4..46fa6fd9f6d 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -38,6 +38,14 @@ module Emails mail_answer_thread(@snippet, note_thread_options(recipient_id)) end + def note_personal_snippet_email(recipient_id, note_id) + setup_note_mail(note_id, recipient_id) + + @snippet = @note.noteable + @target_url = snippet_url(@note.noteable) + mail_answer_thread(@snippet, note_thread_options(recipient_id)) + end + private def note_target_url_options diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 0bc1c19e9cd..0cd3456b4de 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -107,15 +107,11 @@ class Notify < BaseMailer def mail_thread(model, headers = {}) add_project_headers + add_unsubscription_headers_and_links + headers["X-GitLab-#{model.class.name}-ID"] = model.id headers['X-GitLab-Reply-Key'] = reply_key - if !@labels_url && @sent_notification && @sent_notification.unsubscribable? - headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>" - - @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification) - end - if Gitlab::IncomingEmail.enabled? address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address.display_name = @project.name_with_namespace @@ -171,4 +167,16 @@ class Notify < BaseMailer headers['X-GitLab-Project-Id'] = @project.id headers['X-GitLab-Project-Path'] = @project.path_with_namespace end + + def add_unsubscription_headers_and_links + return unless !@labels_url && @sent_notification && @sent_notification.unsubscribable? + + list_unsubscribe_methods = [unsubscribe_sent_notification_url(@sent_notification, force: true)] + if Gitlab::IncomingEmail.enabled? && Gitlab::IncomingEmail.supports_wildcard? + list_unsubscribe_methods << "mailto:#{Gitlab::IncomingEmail.unsubscribe_address(reply_key)}" + end + + headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',') + @sent_notification_url = unsubscribe_sent_notification_url(@sent_notification) + end end diff --git a/app/models/ability.rb b/app/models/ability.rb index fa8f8bc3a5f..ad6c588202e 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -22,6 +22,17 @@ class Ability end end + # Given a list of users and a snippet this method returns the users that can + # read the given snippet. + def users_that_can_read_personal_snippet(users, snippet) + case snippet.visibility_level + when Snippet::INTERNAL, Snippet::PUBLIC + users + when Snippet::PRIVATE + users.include?(snippet.author) ? [snippet.author] : [] + end + end + # Returns an Array of Issues that can be read by the given user. # # issues - The issues to reduce down to those readable by the user. diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bf463a3b6bb..2df8b071e13 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -68,6 +68,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, if: :koding_enabled + validates :plantuml_url, + presence: true, + if: :plantuml_enabled + validates :max_attachment_size, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -152,51 +156,64 @@ class ApplicationSetting < ActiveRecord::Base def self.expire Rails.cache.delete(CACHE_KEY) + rescue + # Gracefully handle when Redis is not available. For example, + # omnibus may fail here during gitlab:assets:compile. end def self.cached Rails.cache.fetch(CACHE_KEY) end - def self.create_from_defaults - create( - default_projects_limit: Settings.gitlab['default_projects_limit'], - default_branch_protection: Settings.gitlab['default_branch_protection'], - signup_enabled: Settings.gitlab['signup_enabled'], - signin_enabled: Settings.gitlab['signin_enabled'], - gravatar_enabled: Settings.gravatar['enabled'], - sign_in_text: nil, + def self.defaults_ce + { after_sign_up_text: nil, - help_page_text: nil, - shared_runners_text: nil, - restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], - max_attachment_size: Settings.gitlab['max_attachment_size'], - session_expire_delay: Settings.gitlab['session_expire_delay'], + akismet_enabled: false, + container_registry_token_expire_delay: 5, + default_branch_protection: Settings.gitlab['default_branch_protection'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_projects_limit: Settings.gitlab['default_projects_limit'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], + disabled_oauth_sign_in_sources: [], domain_whitelist: Settings.gitlab['domain_whitelist'], + gravatar_enabled: Settings.gravatar['enabled'], + help_page_text: nil, + housekeeping_bitmaps_enabled: true, + housekeeping_enabled: true, + housekeeping_full_repack_period: 50, + housekeeping_gc_period: 200, + housekeeping_incremental_repack_period: 10, import_sources: Gitlab::ImportSources.values, - shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], - max_artifacts_size: Settings.artifacts['max_size'], - require_two_factor_authentication: false, - two_factor_grace_period: 48, - recaptcha_enabled: false, - akismet_enabled: false, koding_enabled: false, koding_url: nil, + max_artifacts_size: Settings.artifacts['max_size'], + max_attachment_size: Settings.gitlab['max_attachment_size'], + plantuml_enabled: false, + plantuml_url: nil, + recaptcha_enabled: false, repository_checks_enabled: true, - disabled_oauth_sign_in_sources: [], - send_user_confirmation_email: false, - container_registry_token_expire_delay: 5, repository_storages: ['default'], - user_default_external: false, + require_two_factor_authentication: false, + restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], + session_expire_delay: Settings.gitlab['session_expire_delay'], + send_user_confirmation_email: false, + shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], + shared_runners_text: nil, sidekiq_throttling_enabled: false, - housekeeping_enabled: true, - housekeeping_bitmaps_enabled: true, - housekeeping_incremental_repack_period: 10, - housekeeping_full_repack_period: 50, - housekeeping_gc_period: 200, - ) + sign_in_text: nil, + signin_enabled: Settings.gitlab['signin_enabled'], + signup_enabled: Settings.gitlab['signup_enabled'], + two_factor_grace_period: 48, + user_default_external: false + } + end + + def self.defaults + defaults_ce + end + + def self.create_from_defaults + create(defaults) end def home_page_url_column_exist diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 27042798741..5fe8ddf69d7 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -2,6 +2,7 @@ module Ci class Build < CommitStatus include TokenAuthenticatable include AfterCommitQueue + include Presentable belongs_to :runner belongs_to :trigger_request @@ -91,6 +92,12 @@ module Ci end state_machine :status do + after_transition any => [:pending] do |build| + build.run_after_commit do + BuildQueueWorker.perform_async(id) + end + end + after_transition pending: :running do |build| build.run_after_commit do BuildHooksWorker.perform_async(id) @@ -507,6 +514,10 @@ module Ci end end + def has_expiring_artifacts? + artifacts_expire_at.present? + end + def keep_artifacts! self.update(artifacts_expire_at: nil) end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index abbbddaa4f6..fab8497ec7d 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -128,21 +128,26 @@ module Ci end def stages + # TODO, this needs refactoring, see gitlab-ce#26481. + + stages_query = statuses + .group('stage').select(:stage).order('max(stage_idx)') + status_sql = statuses.latest.where('stage=sg.stage').status_sql - stages_query = statuses.group('stage').select(:stage) - .order('max(stage_idx)') + warnings_sql = statuses.latest.select('COUNT(*) > 0') + .where('stage=sg.stage').failed_but_allowed.to_sql - stages_with_statuses = CommitStatus.from(stages_query, :sg). - pluck('sg.stage', status_sql) + stages_with_statuses = CommitStatus.from(stages_query, :sg) + .pluck('sg.stage', status_sql, "(#{warnings_sql})") stages_with_statuses.map do |stage| - Ci::Stage.new(self, name: stage.first, status: stage.last) + Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)]) end end def artifacts - builds.latest.with_artifacts_not_expired + builds.latest.with_artifacts_not_expired.includes(project: [:namespace]) end def project_id @@ -191,7 +196,11 @@ module Ci end def manual_actions - builds.latest.manual_actions + builds.latest.manual_actions.includes(project: [:namespace]) + end + + def stuck? + builds.pending.any?(&:stuck?) end def retryable? @@ -283,6 +292,10 @@ module Ci end end + def has_yaml_errors? + yaml_errors.present? + end + def environments builds.where.not(environment: nil).success.pluck(:environment).uniq end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 123930273e0..ed1843ba005 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -2,6 +2,7 @@ module Ci class Runner < ActiveRecord::Base extend Ci::Model + RUNNER_QUEUE_EXPIRY_TIME = 60.minutes LAST_CONTACT_TIME = 1.hour.ago AVAILABLE_SCOPES = %w[specific shared active paused online] FORM_EDITABLE = %i[description tag_list active run_untagged locked] @@ -21,6 +22,8 @@ module Ci scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } scope :ordered, ->() { order(id: :desc) } + after_save :tick_runner_queue, if: :form_editable_changed? + scope :owned_or_shared, ->(project_id) do joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) @@ -122,8 +125,38 @@ module Ci ] end + def tick_runner_queue + SecureRandom.hex.tap do |new_update| + Gitlab::Redis.with do |redis| + redis.set(runner_queue_key, new_update, ex: RUNNER_QUEUE_EXPIRY_TIME) + end + end + end + + def ensure_runner_queue_value + Gitlab::Redis.with do |redis| + value = SecureRandom.hex + redis.set(runner_queue_key, value, ex: RUNNER_QUEUE_EXPIRY_TIME, nx: true) + redis.get(runner_queue_key) + end + end + + def is_runner_queue_value_latest?(value) + ensure_runner_queue_value == value if value.present? + end + private + def runner_queue_key + "runner:build_queue:#{self.token}" + end + + def form_editable_changed? + FORM_EDITABLE.any? do |editable| + public_send("#{editable}_changed?") + end + end + def tag_constraints unless has_tags? || run_untagged? errors.add(:tags_list, diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index d035eda6df5..ca74c91b062 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -8,10 +8,11 @@ module Ci delegate :project, to: :pipeline - def initialize(pipeline, name:, status: nil) + def initialize(pipeline, name:, status: nil, warnings: nil) @pipeline = pipeline @name = name @status = status + @warnings = warnings end def to_param @@ -39,5 +40,17 @@ module Ci def builds @builds ||= pipeline.builds.where(stage: name) end + + def success? + status.to_s == 'success' + end + + def has_warnings? + if @warnings.nil? + statuses.latest.failed_but_allowed.any? + else + @warnings + end + end end end diff --git a/app/models/commit.rb b/app/models/commit.rb index 69cfc47f5bf..316bd2e512b 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -21,6 +21,9 @@ class Commit DIFF_HARD_LIMIT_FILES = 1000 DIFF_HARD_LIMIT_LINES = 50000 + # The SHA can be between 7 and 40 hex characters. + COMMIT_SHA_PATTERN = '\h{7,40}' + class << self def decorate(commits, project) commits.map do |commit| @@ -52,6 +55,10 @@ class Commit def from_hash(hash, project) new(Gitlab::Git::Commit.new(hash), project) end + + def valid_hash?(key) + !!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key) + end end attr_accessor :raw @@ -77,8 +84,6 @@ class Commit # Pattern used to extract commit references from text # - # The SHA can be between 7 and 40 hex characters. - # # This pattern supports cross-project references. def self.reference_pattern @reference_pattern ||= %r{ @@ -88,11 +93,11 @@ class Commit end def self.link_reference_pattern - @link_reference_pattern ||= super("commit", /(?<commit>\h{7,40})/) + @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/) end - def to_reference(from_project = nil) - commit_reference(from_project, id) + def to_reference(from_project = nil, full: false) + commit_reference(from_project, id, full: full) end def reference_link_text(from_project = nil) @@ -318,10 +323,24 @@ class Commit Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) end + def persisted? + true + end + + def touch + # no-op but needs to be defined since #persisted? is defined + end + + WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze + + def work_in_progress? + !!(title =~ WIP_REGEX) + end + private - def commit_reference(from_project, referable_commit_id) - reference = project.to_reference(from_project) + def commit_reference(from_project, referable_commit_id, full: false) + reference = project.to_reference(from_project, full: full) if reference.present? "#{reference}#{self.class.reference_prefix}#{referable_commit_id}" diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb index d9af7f6c139..84e2e8a5dd5 100644 --- a/app/models/commit_range.rb +++ b/app/models/commit_range.rb @@ -89,8 +89,8 @@ class CommitRange alias_method :id, :to_s - def to_reference(from_project = nil) - project_reference = project.to_reference(from_project) + def to_reference(from_project = nil, full: false) + project_reference = project.to_reference(from_project, full: full) if project_reference.present? project_reference + self.class.reference_prefix + self.id diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 31cd381dcd2..9547c57b2ae 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -137,4 +137,10 @@ class CommitStatus < ActiveRecord::Base .new(self, current_user) .fabricate! end + + def sortable_name + name.split(/(\d+)/).map do |v| + v =~ /\d+/ ? v.to_i : v + end + end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 90bd6490a02..a600f9c14c5 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -51,6 +51,10 @@ module CacheMarkdownField CACHING_CLASSES.map(&:constantize) end + def skip_project_check? + false + end + extend ActiveSupport::Concern included do @@ -112,7 +116,8 @@ module CacheMarkdownField invalidation_method = "#{html_field}_invalidated?".to_sym define_method(cache_method) do - html = Banzai::Renderer.cacheless_render_field(self, markdown_field) + options = { skip_project_check: skip_project_check? } + html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options) __send__("#{html_field}=", html) true end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 90432fc4050..431c0354969 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -1,6 +1,7 @@ module HasStatus extend ActiveSupport::Concern + DEFAULT_STATUS = 'created' AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] STARTED_STATUSES = %w[running success failed skipped] ACTIVE_STATUSES = %w[pending running] diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5e63825bf99..3517969eabc 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -13,6 +13,7 @@ module Issuable include StripAttribute include Awardable include Taskable + include TimeTrackable included do cache_markdown_field :title, pipeline: :single_line diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 8ab0401d288..ef2c1e5d414 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -49,7 +49,11 @@ module Mentionable self.class.mentionable_attrs.each do |attr, options| text = __send__(attr) - options = options.merge(cache_key: [self, attr], author: author) + options = options.merge( + cache_key: [self, attr], + author: author, + skip_project_check: skip_project_check? + ) extractor.analyze(text, options) end @@ -121,4 +125,8 @@ module Mentionable def cross_reference_exists?(target) SystemNoteService.cross_reference_exists?(target, local_reference) end + + def skip_project_check? + false + end end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 8f02c226f0b..e9450dd0c26 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -7,11 +7,14 @@ module Milestoneish def total_items_count(user) memoize_per_user(user, :total_items_count) do - issues_count = count_issues_by_state(user).values.sum - issues_count + merge_requests.size + total_issues_count(user) + merge_requests.size end end + def total_issues_count(user) + count_issues_by_state(user).values.sum + end + def complete?(user) total_items_count(user) > 0 && total_items_count(user) == closed_items_count(user) end @@ -36,8 +39,8 @@ module Milestoneish def issues_visible_to_user(user) memoize_per_user(user, :issues_visible_to_user) do - params = try(:project_id) ? { project_id: project_id } : {} - IssuesFinder.new(user, params).execute.where(milestone_id: milestoneish_ids) + IssuesFinder.new(user, issues_finder_params) + .execute.where(milestone_id: milestoneish_ids) end end @@ -72,4 +75,10 @@ module Milestoneish @memoized[method_name] ||= {} @memoized[method_name][user.try!(:id)] ||= yield end + + # override in a class that includes this module to get a faster query + # from IssuesFinder + def issues_finder_params + {} + end end diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index 70740c76e43..4865c0a14b1 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -96,6 +96,11 @@ module Participable participants.merge(ext.users) - Ability.users_that_can_read_project(participants.to_a, project) + case self + when PersonalSnippet + Ability.users_that_can_read_personal_snippet(participants.to_a, self) + else + Ability.users_that_can_read_project(participants.to_a, project) + end end end diff --git a/app/models/concerns/presentable.rb b/app/models/concerns/presentable.rb new file mode 100644 index 00000000000..7b33b837004 --- /dev/null +++ b/app/models/concerns/presentable.rb @@ -0,0 +1,7 @@ +module Presentable + def present(**attributes) + Gitlab::View::Presenter::Factory + .new(self, attributes) + .fabricate! + end +end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index 6d88951c713..60734bc6660 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility build_project_feature unless project_feature access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - project_feature.update_attribute(field, access_level) + project_feature.send(:write_attribute, field, access_level) end end diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb index 944519a3070..2589215ad19 100644 --- a/app/models/concerns/reactive_caching.rb +++ b/app/models/concerns/reactive_caching.rb @@ -55,30 +55,30 @@ module ReactiveCaching self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_lifetime = 10.minutes - def calculate_reactive_cache + def calculate_reactive_cache(*args) raise NotImplementedError end - def with_reactive_cache(&blk) - within_reactive_cache_lifetime do - data = Rails.cache.read(full_reactive_cache_key) + def with_reactive_cache(*args, &blk) + within_reactive_cache_lifetime(*args) do + data = Rails.cache.read(full_reactive_cache_key(*args)) yield data if data.present? end ensure - Rails.cache.write(full_reactive_cache_key('alive'), true, expires_in: self.class.reactive_cache_lifetime) - ReactiveCachingWorker.perform_async(self.class, id) + Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime) + ReactiveCachingWorker.perform_async(self.class, id, *args) end - def clear_reactive_cache! - Rails.cache.delete(full_reactive_cache_key) + def clear_reactive_cache!(*args) + Rails.cache.delete(full_reactive_cache_key(*args)) end - def exclusively_update_reactive_cache! - locking_reactive_cache do - within_reactive_cache_lifetime do - enqueuing_update do - value = calculate_reactive_cache - Rails.cache.write(full_reactive_cache_key, value) + def exclusively_update_reactive_cache!(*args) + locking_reactive_cache(*args) do + within_reactive_cache_lifetime(*args) do + enqueuing_update(*args) do + value = calculate_reactive_cache(*args) + Rails.cache.write(full_reactive_cache_key(*args), value) end end end @@ -93,22 +93,26 @@ module ReactiveCaching ([prefix].flatten + qualifiers).join(':') end - def locking_reactive_cache - lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key, timeout: reactive_cache_lease_timeout) + def alive_reactive_cache_key(*qualifiers) + full_reactive_cache_key(*(qualifiers + ['alive'])) + end + + def locking_reactive_cache(*args) + lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key(*args), timeout: reactive_cache_lease_timeout) uuid = lease.try_obtain yield if uuid ensure - Gitlab::ExclusiveLease.cancel(full_reactive_cache_key, uuid) + Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid) end - def within_reactive_cache_lifetime - yield if Rails.cache.read(full_reactive_cache_key('alive')) + def within_reactive_cache_lifetime(*args) + yield if Rails.cache.read(alive_reactive_cache_key(*args)) end - def enqueuing_update + def enqueuing_update(*args) yield ensure - ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id) + ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args) end end end diff --git a/app/models/concerns/reactive_service.rb b/app/models/concerns/reactive_service.rb new file mode 100644 index 00000000000..e1f868a299b --- /dev/null +++ b/app/models/concerns/reactive_service.rb @@ -0,0 +1,10 @@ +module ReactiveService + extend ActiveSupport::Concern + + included do + include ReactiveCaching + + # Default cache key: class name + project_id + self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] } + end +end diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb index 8ba009fe04f..da803c7f481 100644 --- a/app/models/concerns/referable.rb +++ b/app/models/concerns/referable.rb @@ -17,7 +17,7 @@ module Referable # Issue.last.to_reference(other_project) # => "cross-project#1" # # Returns a String - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, full:) '' end diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 1108a64c59e..2b93aa30c0f 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -60,6 +60,21 @@ module Routable joins(:route).where(wheres.join(' OR ')) end end + + # Builds a relation to find multiple objects that are nested under user membership + # + # Usage: + # + # Klass.member_descendants(1) + # + # Returns an ActiveRecord::Relation. + def member_descendants(user_id) + joins(:route). + joins("INNER JOIN routes r2 ON routes.path LIKE CONCAT(r2.path, '/%') + INNER JOIN members ON members.source_id = r2.source_id + AND members.source_type = r2.source_type"). + where('members.user_id = ?', user_id) + end end private diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index ebc75100a54..25e2d8ea24e 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -11,10 +11,10 @@ module Taskable INCOMPLETE = 'incomplete'.freeze ITEM_PATTERN = / ^ - (?:\s*[-+*]|(?:\d+\.))? # optional list prefix - \s* # optional whitespace prefix - (\[\s\]|\[[xX]\]) # checkbox - (\s.+) # followed by whitespace and some text. + \s*(?:[-+*]|(?:\d+\.)) # list prefix required - task item has to be always in a list + \s+ # whitespace prefix has to be always presented for a list item + (\[\s\]|\[[xX]\]) # checkbox + (\s.+) # followed by whitespace and some text. /x def self.get_tasks(content) diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb new file mode 100644 index 00000000000..040e3a2884e --- /dev/null +++ b/app/models/concerns/time_trackable.rb @@ -0,0 +1,72 @@ +# == TimeTrackable concern +# +# Contains functionality related to objects that support time tracking. +# +# Used by Issue and MergeRequest. +# + +module TimeTrackable + extend ActiveSupport::Concern + + included do + attr_reader :time_spent, :time_spent_user + + alias_method :time_spent?, :time_spent + + default_value_for :time_estimate, value: 0, allows_nil: false + + validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false + validate :check_negative_time_spent + + has_many :timelogs, as: :trackable, dependent: :destroy + end + + def spend_time(options) + @time_spent = options[:duration] + @time_spent_user = options[:user] + @original_total_time_spent = nil + + return if @time_spent == 0 + + if @time_spent == :reset + reset_spent_time + else + add_or_subtract_spent_time + end + end + alias_method :spend_time=, :spend_time + + def total_time_spent + timelogs.sum(:time_spent) + end + + def human_total_time_spent + Gitlab::TimeTrackingFormatter.output(total_time_spent) + end + + def human_time_estimate + Gitlab::TimeTrackingFormatter.output(time_estimate) + end + + private + + def reset_spent_time + timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) + end + + def add_or_subtract_spent_time + timelogs.new(time_spent: time_spent, user: @time_spent_user) + end + + def check_negative_time_spent + return if time_spent.nil? || time_spent == :reset + + # we need to cache the total time spent so multiple calls to #valid? + # doesn't give a false error + @original_total_time_spent ||= total_time_spent + + if time_spent < 0 && (time_spent.abs > @original_total_time_spent) + errors.add(:time_spent, 'Time to subtract exceeds the total time spent') + end + end +end diff --git a/app/models/concerns/valid_attribute.rb b/app/models/concerns/valid_attribute.rb new file mode 100644 index 00000000000..8c35cea8d58 --- /dev/null +++ b/app/models/concerns/valid_attribute.rb @@ -0,0 +1,10 @@ +module ValidAttribute + extend ActiveSupport::Concern + + # Checks whether an attribute has failed validation or not + # + # +attribute+ The symbolised name of the attribute i.e :name + def valid_attribute?(attribute) + self.errors.empty? || self.errors.messages[attribute].nil? + end +end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index ba4ee6fcf9d..d2e626c22e8 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -1,62 +1,38 @@ class CycleAnalytics STAGES = %i[issue plan code test review staging production].freeze - def initialize(project, current_user, from:) + def initialize(project, options) @project = project - @current_user = current_user - @from = from - @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil) + @options = options end def summary - @summary ||= Summary.new(@project, @current_user, from: @from) + @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project, + from: @options[:from], + current_user: @options[:current_user]).data end - def permissions(user:) - Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) + def stats + @stats ||= stats_per_stage end - def issue - @fetcher.calculate_metric(:issue, - Issue.arel_table[:created_at], - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]]) + def no_stats? + stats.all? { |hash| hash[:value].nil? } end - def plan - @fetcher.calculate_metric(:plan, - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]], - Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) - end - - def code - @fetcher.calculate_metric(:code, - Issue::Metrics.arel_table[:first_mentioned_in_commit_at], - MergeRequest.arel_table[:created_at]) - end - - def test - @fetcher.calculate_metric(:test, - MergeRequest::Metrics.arel_table[:latest_build_started_at], - MergeRequest::Metrics.arel_table[:latest_build_finished_at]) + def permissions(user:) + Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) end - def review - @fetcher.calculate_metric(:review, - MergeRequest.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:merged_at]) + def [](stage_name) + Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options) end - def staging - @fetcher.calculate_metric(:staging, - MergeRequest::Metrics.arel_table[:merged_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) - end + private - def production - @fetcher.calculate_metric(:production, - Issue.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + def stats_per_stage + STAGES.map do |stage_name| + self[stage_name].as_json + end end end diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb index 82f53d17ddd..e69de29bb2d 100644 --- a/app/models/cycle_analytics/summary.rb +++ b/app/models/cycle_analytics/summary.rb @@ -1,43 +0,0 @@ -class CycleAnalytics - class Summary - def initialize(project, current_user, from:) - @project = project - @current_user = current_user - @from = from - end - - def new_issues - IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count - end - - def commits - ref = @project.default_branch.presence - count_commits_for(ref) - end - - def deploys - @project.deployments.where("created_at > ?", @from).count - end - - private - - # Don't use the `Gitlab::Git::Repository#log` method, because it enforces - # a limit. Since we need a commit count, we _can't_ enforce a limit, so - # the easiest way forward is to replicate the relevant portions of the - # `log` function here. - def count_commits_for(ref) - return unless ref - - repository = @project.repository.raw_repository - sha = @project.repository.commit(ref).sha - - cmd = %W(git --git-dir=#{repository.path} log) - cmd << '--format=%H' - cmd << "--after=#{@from.iso8601}" - cmd << sha - - raw_output = IO.popen(cmd) { |io| io.read } - raw_output.lines.count - end - end -end diff --git a/app/models/dashboard_milestone.rb b/app/models/dashboard_milestone.rb new file mode 100644 index 00000000000..646c1e5ce1a --- /dev/null +++ b/app/models/dashboard_milestone.rb @@ -0,0 +1,5 @@ +class DashboardMilestone < GlobalMilestone + def issues_finder_params + { authorized_only: true } + end +end diff --git a/app/models/environment.rb b/app/models/environment.rb index 5cde94b3509..652abf18a8a 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -87,7 +87,7 @@ class Environment < ActiveRecord::Base end def update_merge_request_metrics? - self.name == "production" + (environment_type || name) == "production" end def first_deployment_for(commit) diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 91b508eb325..26712c19b5a 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -38,7 +38,7 @@ class ExternalIssue @reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)} end - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, full: nil) id end diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb index 9803bae0bee..36cf7ad6a28 100644 --- a/app/models/forked_project_link.rb +++ b/app/models/forked_project_link.rb @@ -1,4 +1,4 @@ class ForkedProjectLink < ActiveRecord::Base - belongs_to :forked_to_project, class_name: Project - belongs_to :forked_from_project, class_name: Project + belongs_to :forked_to_project, class_name: 'Project' + belongs_to :forked_from_project, class_name: 'Project' end diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb index fa54e3540d0..8867ba0d2ff 100644 --- a/app/models/generic_commit_status.rb +++ b/app/models/generic_commit_status.rb @@ -1,6 +1,10 @@ class GenericCommitStatus < CommitStatus before_validation :set_default_values + validates :target_url, addressable_url: true, + length: { maximum: 255 }, + allow_nil: true + # GitHub compatible API alias_attribute :context, :name @@ -12,4 +16,10 @@ class GenericCommitStatus < CommitStatus def tags [:external] end + + def detailed_status(current_user) + Gitlab::Ci::Status::External::Factory + .new(self, current_user) + .fabricate! + end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index a54e478f5f8..b991d78e27f 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -1,6 +1,8 @@ class GlobalMilestone include Milestoneish + EPOCH = DateTime.parse('1970-01-01') + attr_accessor :title, :milestones alias_attribute :name, :title @@ -8,13 +10,22 @@ class GlobalMilestone @first_milestone end - def self.build_collection(milestones) - milestones = milestones.group_by(&:title) + def self.build_collection(projects, params) + child_milestones = MilestonesFinder.new.execute(projects, params) - milestones.map do |title, milestones| - milestones_relation = Milestone.where(id: milestones.map(&:id)) + milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped| + milestones_relation = Milestone.where(id: grouped.map(&:id)) new(title, milestones_relation) end + + milestones.sort_by { |milestone| milestone.due_date || EPOCH } + end + + def self.build(projects, title) + child_milestones = Milestone.of_projects(projects).where(title: title) + return if child_milestones.blank? + + new(title, child_milestones) end def initialize(title, milestones) diff --git a/app/models/group.rb b/app/models/group.rb index 9888b242e98..4cdfd022094 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -80,7 +80,7 @@ class Group < Namespace end end - def to_reference(_from_project = nil) + def to_reference(_from_project = nil, full: nil) "#{self.class.reference_prefix}#{name}" end @@ -201,7 +201,7 @@ class Group < Namespace end def members_with_parents - GroupMember.where(requested_at: nil, source_id: parents.map(&:id).push(id)) + GroupMember.where(requested_at: nil, source_id: ancestors.map(&:id).push(id)) end def users_with_parents diff --git a/app/models/group_label.rb b/app/models/group_label.rb index 68841ace2e6..92c83b54861 100644 --- a/app/models/group_label.rb +++ b/app/models/group_label.rb @@ -8,8 +8,4 @@ class GroupLabel < Label def subject_foreign_key 'group_id' end - - def to_reference(source_project = nil, target_project = nil, format: :id) - super(source_project, target_project, format: format) - end end diff --git a/app/models/group_milestone.rb b/app/models/group_milestone.rb new file mode 100644 index 00000000000..7b6db2634b7 --- /dev/null +++ b/app/models/group_milestone.rb @@ -0,0 +1,19 @@ +class GroupMilestone < GlobalMilestone + attr_accessor :group + + def self.build_collection(group, projects, params) + super(projects, params).each do |milestone| + milestone.group = group + end + end + + def self.build(group, projects, title) + super(projects, title).tap do |milestone| + milestone.group = group if milestone + end + end + + def issues_finder_params + { group_id: group.id } + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 6825553512f..65638d9a299 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -97,10 +97,10 @@ class Issue < ActiveRecord::Base end end - def to_reference(from_project = nil) + def to_reference(from_project = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" - "#{project.to_reference(from_project)}#{reference}" + "#{project.to_reference(from_project, full: full)}#{reference}" end def referenced_merge_requests(current_user = nil) diff --git a/app/models/key.rb b/app/models/key.rb index 6f377f0e8ae..9c74ca84753 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -4,6 +4,8 @@ class Key < ActiveRecord::Base include AfterCommitQueue include Sortable + LAST_USED_AT_REFRESH_TIME = 1.day.to_i + belongs_to :user before_validation :generate_fingerprint @@ -49,6 +51,13 @@ class Key < ActiveRecord::Base "key-#{id}" end + def update_last_used_at + lease = Gitlab::ExclusiveLease.new("key_update_last_used_at:#{id}", timeout: LAST_USED_AT_REFRESH_TIME) + return unless lease.try_obtain + + UseKeyWorker.perform_async(id) + end + def add_to_shell GitlabShellWorker.perform_async( :add_key, diff --git a/app/models/label.rb b/app/models/label.rb index d38c37344c9..5b6b9a7a736 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -26,6 +26,7 @@ class Label < ActiveRecord::Base # Don't allow ',' for label titles validates :title, presence: true, format: { with: /\A[^,]+\z/ } validates :title, uniqueness: { scope: [:group_id, :project_id] } + validates :title, length: { maximum: 255 } default_scope { order(title: :asc) } @@ -146,17 +147,17 @@ class Label < ActiveRecord::Base # # Label.first.to_reference # => "~1" # Label.first.to_reference(format: :name) # => "~\"bug\"" - # Label.first.to_reference(project, same_namespace_project) # => "gitlab-ce~1" - # Label.first.to_reference(project, another_namespace_project) # => "gitlab-org/gitlab-ce~1" + # Label.first.to_reference(project, target_project: same_namespace_project) # => "gitlab-ce~1" + # Label.first.to_reference(project, target_project: another_namespace_project) # => "gitlab-org/gitlab-ce~1" # # Returns a String # - def to_reference(source_project = nil, target_project = nil, format: :id) + def to_reference(from_project = nil, target_project: nil, format: :id, full: false) format_reference = label_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - if source_project - "#{source_project.to_reference(target_project)}#{reference}" + if from_project + "#{from_project.to_reference(target_project, full: full)}#{reference}" else reference end diff --git a/app/models/member.rb b/app/models/member.rb index c585e0b450e..26a6054e00d 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -68,9 +68,9 @@ class Member < ActiveRecord::Base after_create :send_request, if: :request?, unless: :importing? after_create :create_notification_setting, unless: [:pending?, :importing?] after_create :post_create_hook, unless: [:pending?, :importing?] - after_create :refresh_member_authorized_projects, if: :importing? after_update :post_update_hook, unless: [:pending?, :importing?] after_destroy :post_destroy_hook, unless: :pending? + after_commit :refresh_member_authorized_projects delegate :name, :username, :email, to: :user, prefix: true @@ -147,8 +147,6 @@ class Member < ActiveRecord::Base member.save end - UserProjectAccessChangedService.new(user.id).execute if user.is_a?(User) - member end @@ -275,23 +273,27 @@ class Member < ActiveRecord::Base end def post_create_hook - UserProjectAccessChangedService.new(user.id).execute system_hook_service.execute_hooks_for(self, :create) end def post_update_hook - UserProjectAccessChangedService.new(user.id).execute if access_level_changed? + # override in sub class end def post_destroy_hook - refresh_member_authorized_projects system_hook_service.execute_hooks_for(self, :destroy) end + # Refreshes authorizations of the current member. + # + # This method schedules a job using Sidekiq and as such **must not** be called + # in a transaction. Doing so can lead to the job running before the + # transaction has been committed, resulting in the job either throwing an + # error or not doing any meaningful work. def refresh_member_authorized_projects - # If user/source is being destroyed, project access are gonna be destroyed eventually - # because of DB foreign keys, so we shouldn't bother with refreshing after each - # member is destroyed through association + # If user/source is being destroyed, project access are going to be + # destroyed eventually because of DB foreign keys, so we shouldn't bother + # with refreshing after each member is destroyed through association return if destroyed_by_association.present? UserProjectAccessChangedService.new(user_id).execute diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 61845bf4036..6753504acff 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -91,6 +91,10 @@ class MergeRequest < ActiveRecord::Base around_transition do |merge_request, transition, block| Gitlab::Timeless.timeless(merge_request, &block) end + + after_transition unchecked: :cannot_be_merged do |merge_request, transition| + TodoService.new.merge_request_became_unmergeable(merge_request) + end end validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?] @@ -175,10 +179,10 @@ class MergeRequest < ActiveRecord::Base work_in_progress?(title) ? title : "WIP: #{title}" end - def to_reference(from_project = nil) + def to_reference(from_project = nil, full: false) reference = "#{self.class.reference_prefix}#{iid}" - "#{project.to_reference(from_project)}#{reference}" + "#{project.to_reference(from_project, full: full)}#{reference}" end def first_commit @@ -221,7 +225,7 @@ class MergeRequest < ActiveRecord::Base # true base commit, so we can't simply have `#diff_base_commit` fall back on # this method. def likely_diff_base_commit - first_commit.parent || first_commit + first_commit.try(:parent) || first_commit end def diff_start_commit @@ -861,9 +865,11 @@ class MergeRequest < ActiveRecord::Base paths: paths ) - active_diff_notes.each do |note| - service.execute(note) - Gitlab::Timeless.timeless(note, &:save) + transaction do + active_diff_notes.each do |note| + service.execute(note) + Gitlab::Timeless.timeless(note, &:save) + end end end @@ -898,10 +904,22 @@ class MergeRequest < ActiveRecord::Base end def has_commits? - commits_count > 0 + merge_request_diff && commits_count > 0 end def has_no_commits? !has_commits? end + + def mergeable_with_slash_command?(current_user, autocomplete_precheck: false, last_diff_sha: nil) + return false unless can_be_merged_by?(current_user) + + return true if autocomplete_precheck + + return false unless mergeable?(skip_ci_check: true) + return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?) + return false if last_diff_sha != diff_head_sha + + true + end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index b8f36a2c958..dadb81f9b6e 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -1,7 +1,7 @@ class MergeRequestDiff < ActiveRecord::Base include Sortable include Importable - include EncodingHelper + include Gitlab::Git::EncodingHelper # Prevent store of diff if commits amount more then 500 COMMITS_SAFE_SIZE = 100 @@ -234,28 +234,28 @@ class MergeRequestDiff < ActiveRecord::Base # and save it as array of hashes in st_diffs db field def save_diffs new_attributes = {} - new_diffs = [] if commits.size.zero? new_attributes[:state] = :empty else diff_collection = compare.diffs(Commit.max_diff_options) - - if diff_collection.overflow? - # Set our state to 'overflow' to make the #empty? and #collected? - # methods (generated by StateMachine) return false. - new_attributes[:state] = :overflow - end - - new_attributes[:real_size] = diff_collection.real_size + new_attributes[:real_size] = compare.diffs.real_size if diff_collection.any? new_diffs = dump_diffs(diff_collection) new_attributes[:state] = :collected end + + new_attributes[:st_diffs] = new_diffs || [] + + # Set our state to 'overflow' to make the #empty? and #collected? + # methods (generated by StateMachine) return false. + # + # This attribution has to come at the end of the method so 'overflow' + # state does not get overridden by 'collected'. + new_attributes[:state] = :overflow if diff_collection.overflow? end - new_attributes[:st_diffs] = new_diffs update_columns_serialized(new_attributes) end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 0dcfec89f14..7331000a9f2 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -118,11 +118,11 @@ class Milestone < ActiveRecord::Base # Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1" # Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1" # - def to_reference(from_project = nil, format: :iid) + def to_reference(from_project = nil, format: :iid, full: false) format_reference = milestone_format_reference(format) reference = "#{self.class.reference_prefix}#{format_reference}" - "#{project.to_reference(from_project)}#{reference}" + "#{project.to_reference(from_project, full: full)}#{reference}" end def reference_link_text(from_project = nil) @@ -202,4 +202,8 @@ class Milestone < ActiveRecord::Base errors.add(:start_date, "Can't be greater than due date") end end + + def issues_finder_params + { project_id: project_id } + end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index d41833de66f..67d8c1c2e4c 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -4,6 +4,7 @@ class Namespace < ActiveRecord::Base include CacheMarkdownField include Sortable include Gitlab::ShellAdapter + include Gitlab::CurrentSettings include Routable cache_markdown_field :description, pipeline: :description @@ -130,6 +131,8 @@ class Namespace < ActiveRecord::Base Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + remove_exports! + # If repositories moved successfully we need to # send update instructions to users. # However we cannot allow rollback since we moved namespace dir @@ -174,6 +177,10 @@ class Namespace < ActiveRecord::Base end end + def shared_runners_enabled? + projects.with_shared_runners.any? + end + def full_name @full_name ||= if parent @@ -183,8 +190,26 @@ class Namespace < ActiveRecord::Base end end - def parents - @parents ||= parent ? parent.parents + [parent] : [] + # Scopes the model on ancestors of the record + def ancestors + if parent_id + path = route.path + paths = [] + + until path.blank? + path = path.rpartition('/').first + paths << path + end + + self.class.joins(:route).where('routes.path IN (?)', paths).reorder('routes.path ASC') + else + self.class.none + end + end + + # 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') end private @@ -214,6 +239,8 @@ class Namespace < ActiveRecord::Base GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) end end + + remove_exports! end def refresh_access_of_projects_invited_groups @@ -226,4 +253,20 @@ class Namespace < ActiveRecord::Base def full_path_changed? path_changed? || parent_id_changed? end + + def remove_exports! + Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) + end + + def export_path + File.join(Gitlab::ImportExport.storage_path, full_path_was) + end + + def full_path_was + if parent + parent.full_path + '/' + path_was + else + path_was + end + end end diff --git a/app/models/note.rb b/app/models/note.rb index 0c1b05dabf2..bf090a0438c 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -43,7 +43,8 @@ class Note < ActiveRecord::Base delegate :name, :email, to: :author, prefix: true delegate :title, to: :noteable, allow_nil: true - validates :note, :project, presence: true + validates :note, presence: true + validates :project, presence: true, unless: :for_personal_snippet? # Attachments are deprecated and are handled by Markdown uploader validates :attachment, file_size: { maximum: :max_attachment_size } @@ -53,7 +54,7 @@ class Note < ActiveRecord::Base validates :commit_id, presence: true, if: :for_commit? validates :author, presence: true - validate unless: [:for_commit?, :importing?] do |note| + validate unless: [:for_commit?, :importing?, :for_personal_snippet?] do |note| unless note.noteable.try(:project) == note.project errors.add(:invalid_project, 'Note and noteable project mismatch') end @@ -83,7 +84,7 @@ class Note < ActiveRecord::Base after_initialize :ensure_discussion_id before_validation :nullify_blank_type, :nullify_blank_line_code before_validation :set_discussion_id - after_save :keep_around_commit + after_save :keep_around_commit, unless: :for_personal_snippet? class << self def model_name @@ -165,6 +166,14 @@ class Note < ActiveRecord::Base noteable_type == "Snippet" end + def for_personal_snippet? + noteable.is_a?(PersonalSnippet) + end + + def skip_project_check? + for_personal_snippet? + end + # override to return commits, which are not active record def noteable if for_commit? @@ -220,6 +229,10 @@ class Note < ActiveRecord::Base note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1] end + def to_ability_name + for_personal_snippet? ? 'personal_snippet' : noteable_type.underscore + end + private def keep_around_commit diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb index 43fc218de2b..58f6214bea7 100644 --- a/app/models/notification_setting.rb +++ b/app/models/notification_setting.rb @@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base :success_pipeline ] + EXCLUDED_WATCHER_EVENTS = [ + :success_pipeline + ] + store :events, accessors: EMAIL_EVENTS, coder: JSON before_create :set_events diff --git a/app/models/project.rb b/app/models/project.rb index e0ffa7e7af7..59faf35e051 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -12,6 +12,7 @@ class Project < ActiveRecord::Base include AfterCommitQueue include CaseSensitivity include TokenAuthenticatable + include ValidAttribute include ProjectFeaturesCompatibility include SelectForProjectAuthorization include Routable @@ -65,6 +66,8 @@ class Project < ActiveRecord::Base end end + after_validation :check_pending_delete + ActsAsTaggableOn.strict_case_match = true acts_as_taggable_on :tags @@ -118,8 +121,6 @@ class Project < ActiveRecord::Base # Merge Requests for target project should be removed with it has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' - # Merge requests from source project should be kept when source project was removed - has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest has_many :issues, dependent: :destroy has_many :labels, dependent: :destroy, class_name: 'ProjectLabel' has_many :services, dependent: :destroy @@ -130,7 +131,7 @@ class Project < ActiveRecord::Base has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :protected_branches, dependent: :destroy - has_many :project_authorizations, dependent: :destroy + has_many :project_authorizations has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source alias_method :members, :project_members @@ -223,6 +224,7 @@ class Project < ActiveRecord::Base scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_statistics, -> { includes(:statistics) } + scope :with_shared_runners, -> { where(shared_runners_enabled: true) } # "enabled" here means "not disabled". It includes private features! scope :with_feature_enabled, ->(feature) { @@ -589,8 +591,8 @@ class Project < ActiveRecord::Base end end - def to_reference(from_project = nil) - if cross_namespace_reference?(from_project) + def to_reference(from_project = nil, full: false) + if full || cross_namespace_reference?(from_project) path_with_namespace elsif cross_project_reference?(from_project) path @@ -609,10 +611,6 @@ class Project < ActiveRecord::Base Gitlab::Routing.url_helpers.namespace_project_url(self.namespace, self) end - def web_url_without_protocol - web_url.split('://')[1] - end - def new_issue_address(author) return unless Gitlab::IncomingEmail.supports_issue_creation? && author @@ -1033,7 +1031,7 @@ class Project < ActiveRecord::Base "refs/heads/#{branch}", force: true) repository.copy_gitattributes(branch) - repository.expire_avatar_cache + repository.after_change_head reload_default_branch end @@ -1099,12 +1097,20 @@ class Project < ActiveRecord::Base project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) end + def shared_runners_available? + shared_runners_enabled? + end + + def shared_runners + shared_runners_available? ? Ci::Runner.shared : Ci::Runner.none + end + def any_runners?(&block) if runners.active.any?(&block) return true end - shared_runners_enabled? && Ci::Runner.shared.active.any?(&block) + shared_runners.active.any?(&block) end def valid_runners_token?(token) @@ -1324,4 +1330,21 @@ class Project < ActiveRecord::Base stats = statistics || build_statistics stats.update(namespace_id: namespace_id) end + + def check_pending_delete + return if valid_attribute?(:name) && valid_attribute?(:path) + return unless pending_delete_twin + + %i[route route.path name path].each do |error| + errors.delete(error) + end + + errors.add(:base, "The project is still being deleted. Please try again later.") + end + + def pending_delete_twin + return false unless path + + Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace) + end end diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index 6149c35cc61..5cb6b0c527d 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -16,8 +16,7 @@ class ProjectGroupLink < ActiveRecord::Base validates :group_access, inclusion: { in: Gitlab::Access.values }, presence: true validate :different_group - after_create :refresh_group_members_authorized_projects - after_destroy :refresh_group_members_authorized_projects + after_commit :refresh_group_members_authorized_projects def self.access_options Gitlab::Access.options diff --git a/app/models/project_label.rb b/app/models/project_label.rb index 82f47f0e8fd..313815e5869 100644 --- a/app/models/project_label.rb +++ b/app/models/project_label.rb @@ -16,8 +16,8 @@ class ProjectLabel < Label 'project_id' end - def to_reference(target_project = nil, format: :id) - super(project, target_project, format: format) + def to_reference(target_project = nil, format: :id, full: false) + super(project, target_project: target_project, format: format, full: full) end private diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 7c23b766763..3728f5642e4 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -25,7 +25,7 @@ You can create a Personal Access Token here: http://app.asana.com/-/account_api' end - def to_param + def self.to_param 'asana' end @@ -44,7 +44,7 @@ http://app.asana.com/-/account_api' ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb index d839221d315..aeeff8917bf 100644 --- a/app/models/project_services/assembla_service.rb +++ b/app/models/project_services/assembla_service.rb @@ -12,7 +12,7 @@ class AssemblaService < Service 'Project Management Software (Source Commits Endpoint)' end - def to_param + def self.to_param 'assembla' end @@ -23,7 +23,7 @@ class AssemblaService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index b5c76e4d4fe..400020ee04a 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -1,4 +1,6 @@ class BambooService < CiService + include ReactiveService + prop_accessor :bamboo_url, :build_key, :username, :password validates :bamboo_url, presence: true, url: true, if: :activated? @@ -38,7 +40,7 @@ class BambooService < CiService 'You must set up automatic revision labeling and a repository trigger in Bamboo.' end - def to_param + def self.to_param 'bamboo' end @@ -54,35 +56,46 @@ class BambooService < CiService ] end - def supported_events - %w(push) + def build_page(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end - def build_info(sha) - @response = get_path("rest/api/latest/result?label=#{sha}") + def commit_status(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } end - def build_page(sha, ref) - build_info(sha) if @response.nil? || !@response.code + def execute(data) + return unless supported_events.include?(data[:object_kind]) + + get_path("updateAndBuild.action?buildKey=#{build_key}") + end + + def calculate_reactive_cache(sha, ref) + response = get_path("rest/api/latest/result?label=#{sha}") + + { build_page: read_build_page(response), commit_status: read_commit_status(response) } + end - if @response.code != 200 || @response['results']['results']['size'] == '0' + private + + def read_build_page(response) + if response.code != 200 || response['results']['results']['size'] == '0' # If actual build link can't be determined, send user to build summary page. URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s else # If actual build link is available, go to build result page. - result_key = @response['results']['results']['result']['planResultKey']['key'] + result_key = response['results']['results']['result']['planResultKey']['key'] URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s end end - def commit_status(sha, ref) - build_info(sha) if @response.nil? || !@response.code - return :error unless @response.code == 200 || @response.code == 404 + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 - status = if @response.code == 404 || @response['results']['results']['size'] == '0' + status = if response.code == 404 || response['results']['results']['size'] == '0' 'Pending' else - @response['results']['results']['result']['buildState'] + response['results']['results']['result']['buildState'] end if status.include?('Success') @@ -96,14 +109,6 @@ class BambooService < CiService end end - def execute(data) - return unless supported_events.include?(data[:object_kind]) - - get_path("updateAndBuild.action?buildKey=#{build_key}") - end - - private - def build_url(path) URI.join("#{bamboo_url}/", path).to_s end diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb index 338e685339a..046e2809f45 100644 --- a/app/models/project_services/bugzilla_service.rb +++ b/app/models/project_services/bugzilla_service.rb @@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService end end - def to_param + def self.to_param 'bugzilla' end end diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb index fe6d7aabb22..0956c4a4ede 100644 --- a/app/models/project_services/buildkite_service.rb +++ b/app/models/project_services/buildkite_service.rb @@ -1,6 +1,8 @@ require "addressable/uri" class BuildkiteService < CiService + include ReactiveService + ENDPOINT = "https://buildkite.com" prop_accessor :project_url, :token @@ -22,10 +24,6 @@ class BuildkiteService < CiService hook.save end - def supported_events - %w(push) - end - def execute(data) return unless supported_events.include?(data[:object_kind]) @@ -33,13 +31,7 @@ class BuildkiteService < CiService end def commit_status(sha, ref) - response = HTTParty.get(commit_status_path(sha), verify: false) - - if response.code == 200 && response['status'] - response['status'] - else - :error - end + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } end def commit_status_path(sha) @@ -58,7 +50,7 @@ class BuildkiteService < CiService 'Continuous integration and deployments' end - def to_param + def self.to_param 'buildkite' end @@ -78,6 +70,19 @@ class BuildkiteService < CiService ] end + def calculate_reactive_cache(sha, ref) + response = HTTParty.get(commit_status_path(sha), verify: false) + + status = + if response.code == 200 && response['status'] + response['status'] + else + :error + end + + { commit_status: status } + end + private def webhook_token diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 201b94b065b..ebd21e37189 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -19,11 +19,11 @@ class BuildsEmailService < Service 'Email the builds status to a list of recipients.' end - def to_param + def self.to_param 'builds_email' end - def supported_events + def self.supported_events %w(build) end diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 5af93860d09..0de59af5652 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -12,7 +12,7 @@ class CampfireService < Service 'Simple web-based real-time group chat' end - def to_param + def self.to_param 'campfire' end @@ -24,7 +24,7 @@ class CampfireService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb index b7ef44c3054..8468934425f 100644 --- a/app/models/project_services/chat_notification_service.rb +++ b/app/models/project_services/chat_notification_service.rb @@ -25,7 +25,7 @@ class ChatNotificationService < Service valid? end - def supported_events + def self.supported_events %w[push issue confidential_issue merge_request note tag_push build pipeline wiki_page] end @@ -82,19 +82,19 @@ class ChatNotificationService < Service def get_message(object_kind, data) case object_kind when "push", "tag_push" - PushMessage.new(data) + ChatMessage::PushMessage.new(data) when "issue" - IssueMessage.new(data) unless is_update?(data) + ChatMessage::IssueMessage.new(data) unless is_update?(data) when "merge_request" - MergeMessage.new(data) unless is_update?(data) + ChatMessage::MergeMessage.new(data) unless is_update?(data) when "note" - NoteMessage.new(data) + ChatMessage::NoteMessage.new(data) when "build" - BuildMessage.new(data) if should_build_be_notified?(data) + ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data) when "pipeline" - PipelineMessage.new(data) if should_pipeline_be_notified?(data) + ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) when "wiki_page" - WikiPageMessage.new(data) + ChatMessage::WikiPageMessage.new(data) end end diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 0bc160af604..2bcff541cc0 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -13,8 +13,8 @@ class ChatSlashCommandsService < Service ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) end - def supported_events - [] + def self.supported_events + %w() end def can_test? diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb index 596c00705ad..82979c8bd34 100644 --- a/app/models/project_services/ci_service.rb +++ b/app/models/project_services/ci_service.rb @@ -8,19 +8,11 @@ class CiService < Service self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token) end - def supported_events + def self.supported_events %w(push) end - def merge_request_page(iid, sha, ref) - commit_page(sha, ref) - end - - def commit_page(sha, ref) - build_page(sha, ref) - end - - # Return complete url to merge_request page + # Return complete url to build page # # Ex. # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c @@ -35,23 +27,6 @@ class CiService < Service # # # Ex. - # @service.merge_request_status(9, '13be4ac', 'dev') - # # => 'success' - # - # @service.merge_request_status(10, '2abe4ac', 'dev) - # # => 'running' - # - # - def merge_request_status(iid, sha, ref) - commit_status(sha, ref) - end - - # Return string with build status or :error symbol - # - # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' - # - # - # Ex. # @service.commit_status('13be4ac', 'master') # # => 'success' # diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb index b2f426dc2ac..dea915a4d05 100644 --- a/app/models/project_services/custom_issue_tracker_service.rb +++ b/app/models/project_services/custom_issue_tracker_service.rb @@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService end end - def to_param + def self.to_param 'custom_issue_tracker' end diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb index ab353a1abe6..91a55514a9a 100644 --- a/app/models/project_services/deployment_service.rb +++ b/app/models/project_services/deployment_service.rb @@ -5,8 +5,8 @@ class DeploymentService < Service default_value_for :category, 'deployment' - def supported_events - [] + def self.supported_events + %w() end def predefined_variables diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb index adc78a427ee..0a217d8caba 100644 --- a/app/models/project_services/drone_ci_service.rb +++ b/app/models/project_services/drone_ci_service.rb @@ -1,4 +1,6 @@ class DroneCiService < CiService + include ReactiveService + prop_accessor :drone_url, :token boolean_accessor :enable_ssl_verification @@ -30,18 +32,10 @@ class DroneCiService < CiService true end - def supported_events + def self.supported_events %w(push merge_request tag_push) end - def merge_request_status_path(iid, sha = nil, ref = nil) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}", - "?access_token=#{token}"] - - URI.join(*url).to_s - end - def commit_status_path(sha, ref) url = [drone_url, "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", @@ -50,54 +44,34 @@ class DroneCiService < CiService URI.join(*url).to_s end - def merge_request_status(iid, sha, ref) - response = HTTParty.get(merge_request_status_path(iid), verify: enable_ssl_verification) - - if response.code == 200 and response['status'] - case response['status'] - when 'killed' - :canceled - when 'failure', 'error' - # Because drone return error if some test env failed - :failed - else - response["status"] - end - else - :error - end - rescue Errno::ECONNREFUSED - :error + def commit_status(sha, ref) + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } end - def commit_status(sha, ref) + def calculate_reactive_cache(sha, ref) response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification) - if response.code == 200 and response['status'] - case response['status'] - when 'killed' - :canceled - when 'failure', 'error' - # Because drone return error if some test env failed - :failed + status = + if response.code == 200 and response['status'] + case response['status'] + when 'killed' + :canceled + when 'failure', 'error' + # Because drone return error if some test env failed + :failed + else + response["status"] + end else - response["status"] + :error end - else - :error - end - rescue Errno::ECONNREFUSED - :error - end - def merge_request_page(iid, sha, ref) - url = [drone_url, - "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"] - - URI.join(*url).to_s + { commit_status: status } + rescue Errno::ECONNREFUSED + { commit_status: :error } end - def commit_page(sha, ref) + def build_page(sha, ref) url = [drone_url, "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", "?branch=#{URI::encode(ref.to_s)}"] @@ -105,14 +79,6 @@ class DroneCiService < CiService URI.join(*url).to_s end - def commit_coverage(sha, ref) - nil - end - - def build_page(sha, ref) - commit_page(sha, ref) - end - def title 'Drone CI' end @@ -121,7 +87,7 @@ class DroneCiService < CiService 'Drone is a Continuous Integration platform built on Docker, written in Go' end - def to_param + def self.to_param 'drone_ci' end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 79285cbd26d..f4f913ee0b6 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -12,11 +12,11 @@ class EmailsOnPushService < Service 'Email the commits and diff of each push to a list of recipients.' end - def to_param + def self.to_param 'emails_on_push' end - def supported_events + def self.supported_events %w(push tag_push) end diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb index d7b6e505191..bdf6fa6a586 100644 --- a/app/models/project_services/external_wiki_service.rb +++ b/app/models/project_services/external_wiki_service.rb @@ -13,7 +13,7 @@ class ExternalWikiService < Service 'Replaces the link to the internal wiki with a link to an external wiki.' end - def to_param + def self.to_param 'external_wiki' end @@ -29,4 +29,8 @@ class ExternalWikiService < Service nil end end + + def self.supported_events + %w() + end end diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index dd00275187f..10a13c3fbdc 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -12,7 +12,7 @@ class FlowdockService < Service 'Flowdock is a collaboration web app for technical teams.' end - def to_param + def self.to_param 'flowdock' end @@ -22,7 +22,7 @@ class FlowdockService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 598aca5e06d..f271e1f1739 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -12,7 +12,7 @@ class GemnasiumService < Service 'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.' end - def to_param + def self.to_param 'gemnasium' end @@ -23,7 +23,7 @@ class GemnasiumService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb index 6bd8d4ec568..ad4eb9536e1 100644 --- a/app/models/project_services/gitlab_issue_tracker_service.rb +++ b/app/models/project_services/gitlab_issue_tracker_service.rb @@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService default_value_for :default, true - def to_param + def self.to_param 'gitlab' end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 915f6fed74c..72da219df28 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -27,7 +27,7 @@ class HipchatService < Service 'Private group chat and IM' end - def to_param + def self.to_param 'hipchat' end @@ -45,7 +45,7 @@ class HipchatService < Service ] end - def supported_events + def self.supported_events %w(push issue confidential_issue merge_request note tag_push build) end diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index 7355918feab..5d93064f9b3 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -17,11 +17,11 @@ class IrkerService < Service 'gateway.' end - def to_param + def self.to_param 'irker' end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index bce2cdd5516..9e65fdbf9d6 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -57,7 +57,7 @@ class IssueTrackerService < Service end end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 2d969d2fcb6..2ac76e97de0 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -12,7 +12,7 @@ class JiraService < IssueTrackerService # This is confusing, but JiraService does not really support these events. # The values here are required to display correct options in the service # configuration screen. - def supported_events + def self.supported_events %w(commit merge_request) end @@ -81,7 +81,7 @@ class JiraService < IssueTrackerService end end - def to_param + def self.to_param 'jira' end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index 085125ca9dc..fa3cedc4354 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -52,7 +52,7 @@ class KubernetesService < DeploymentService 'deployments with `app=$CI_ENVIRONMENT_SLUG`' end - def to_param + def self.to_param 'kubernetes' end diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb index ee8a0b55275..4ebc5318da1 100644 --- a/app/models/project_services/mattermost_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -7,7 +7,7 @@ class MattermostService < ChatNotificationService 'Receive event notifications in Mattermost' end - def to_param + def self.to_param 'mattermost' end @@ -36,6 +36,6 @@ class MattermostService < ChatNotificationService end def default_channel_placeholder - "#town-square" + "town-square" end end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index 2cb481182d7..50a011db74e 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -15,7 +15,7 @@ class MattermostSlashCommandsService < ChatSlashCommandsService "Perform common operations on GitLab in Mattermost" end - def to_param + def self.to_param 'mattermost_slash_commands' end diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 745f9bd1b43..ac617f409d9 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -15,11 +15,11 @@ class PipelinesEmailService < Service 'Email the pipelines status to a list of recipients.' end - def to_param + def self.to_param 'pipelines_email' end - def supported_events + def self.supported_events %w[pipeline] end diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb index 5301f9fa0ff..9cc642591f4 100644 --- a/app/models/project_services/pivotaltracker_service.rb +++ b/app/models/project_services/pivotaltracker_service.rb @@ -14,7 +14,7 @@ class PivotaltrackerService < Service 'Project Management Software (Source Commits Endpoint)' end - def to_param + def self.to_param 'pivotaltracker' end @@ -34,7 +34,7 @@ class PivotaltrackerService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb index 3dd878e4c7d..a963d27a376 100644 --- a/app/models/project_services/pushover_service.rb +++ b/app/models/project_services/pushover_service.rb @@ -13,7 +13,7 @@ class PushoverService < Service 'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.' end - def to_param + def self.to_param 'pushover' end @@ -61,7 +61,7 @@ class PushoverService < Service ] end - def supported_events + def self.supported_events %w(push) end diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb index f9da273cf08..6acf611eba5 100644 --- a/app/models/project_services/redmine_service.rb +++ b/app/models/project_services/redmine_service.rb @@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService end end - def to_param + def self.to_param 'redmine' end end diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb index 76d233a3cca..f77d2d7c60b 100644 --- a/app/models/project_services/slack_service.rb +++ b/app/models/project_services/slack_service.rb @@ -7,7 +7,7 @@ class SlackService < ChatNotificationService 'Receive event notifications in Slack' end - def to_param + def self.to_param 'slack' end diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index 5a7cc0fb329..c34991e4262 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -9,7 +9,7 @@ class SlackSlashCommandsService < ChatSlashCommandsService "Perform common operations on GitLab in Slack" end - def to_param + def self.to_param 'slack_slash_commands' end diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb index a4a967c9bc9..cbaffb8ce48 100644 --- a/app/models/project_services/teamcity_service.rb +++ b/app/models/project_services/teamcity_service.rb @@ -1,4 +1,6 @@ class TeamcityService < CiService + include ReactiveService + prop_accessor :teamcity_url, :build_type, :username, :password validates :teamcity_url, presence: true, url: true, if: :activated? @@ -41,14 +43,10 @@ class TeamcityService < CiService 'requests build, that setting is in the vsc root advanced settings.' end - def to_param + def self.to_param 'teamcity' end - def supported_events - %w(push) - end - def fields [ { type: 'text', name: 'teamcity_url', @@ -61,43 +59,18 @@ class TeamcityService < CiService ] end - def build_info(sha) - @response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") - end - def build_page(sha, ref) - build_info(sha) if @response.nil? || !@response.code - - if @response.code != 200 - # If actual build link can't be determined, - # send user to build summary page. - build_url("viewLog.html?buildTypeId=#{build_type}") - else - # If actual build link is available, go to build result page. - built_id = @response['build']['id'] - build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}") - end + with_reactive_cache(sha, ref) {|cached| cached[:build_page] } end def commit_status(sha, ref) - build_info(sha) if @response.nil? || !@response.code - return :error unless @response.code == 200 || @response.code == 404 + with_reactive_cache(sha, ref) {|cached| cached[:commit_status] } + end - status = if @response.code == 404 - 'Pending' - else - @response['build']['status'] - end + def calculate_reactive_cache(sha, ref) + response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}") - if status.include?('SUCCESS') - 'success' - elsif status.include?('FAILURE') - 'failed' - elsif status.include?('Pending') - 'pending' - else - :error - end + { build_page: read_build_page(response), commit_status: read_commit_status(response) } end def execute(data) @@ -122,6 +95,40 @@ class TeamcityService < CiService private + def read_build_page(response) + if response.code != 200 + # If actual build link can't be determined, + # send user to build summary page. + build_url("viewLog.html?buildTypeId=#{build_type}") + else + # If actual build link is available, go to build result page. + built_id = response['build']['id'] + build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}") + end + end + + def read_commit_status(response) + return :error unless response.code == 200 || response.code == 404 + + status = if response.code == 404 + 'Pending' + else + response['build']['status'] + end + + return :error unless status.present? + + if status.include?('SUCCESS') + 'success' + elsif status.include?('FAILURE') + 'failed' + elsif status.include?('Pending') + 'pending' + else + :error + end + end + def build_url(path) URI.join("#{teamcity_url}/", path).to_s end diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb index 2270ac75071..06abd406523 100644 --- a/app/models/project_statistics.rb +++ b/app/models/project_statistics.rb @@ -25,8 +25,9 @@ class ProjectStatistics < ActiveRecord::Base self.commit_count = project.repository.commit_count end + # Repository#size needs to be converted from MB to Byte. def update_repository_size - self.repository_size = project.repository.size + self.repository_size = project.repository.size * 1.megabyte end def update_lfs_objects_size diff --git a/app/models/repository.rb b/app/models/repository.rb index 3266e9c75f0..d77b7692d75 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -439,6 +439,11 @@ class Repository expire_content_cache end + # Runs code after the HEAD of a repository is changed. + def after_change_head + expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys) + end + # Runs code after a repository has been forked/imported. def after_import expire_content_cache @@ -1183,7 +1188,18 @@ class Repository end def tags_sorted_by_committed_date - tags.sort_by { |tag| tag.dereferenced_target.committed_date } + tags.sort_by do |tag| + # Annotated tags can point to any object (e.g. a blob), but generally + # tags point to a commit. If we don't have a commit, then just default + # to putting the tag at the end of the list. + target = tag.dereferenced_target + + if target + target.committed_date + else + Time.now + end + end end def keep_around_ref_name(sha) diff --git a/app/models/route.rb b/app/models/route.rb index caf596efa79..dd171fdb069 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -8,15 +8,16 @@ class Route < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } - after_update :rename_children, if: :path_changed? + after_update :rename_descendants, if: :path_changed? - def rename_children + def rename_descendants # We update each row separately because MySQL does not have regexp_replace. # rubocop:disable Rails/FindEach Route.where('path LIKE ?', "#{path_was}/%").each do |route| # Note that update column skips validation and callbacks. - # We need this to avoid recursive call of rename_children method + # We need this to avoid recursive call of rename_descendants method route.update_column(:path, route.path.sub(path_was, path)) end + # rubocop:enable Rails/FindEach end end diff --git a/app/models/service.rb b/app/models/service.rb index 19ef3ba9c23..043be222f3a 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -76,6 +76,11 @@ class Service < ActiveRecord::Base def to_param # implement inside child + self.class.to_param + end + + def self.to_param + raise NotImplementedError end def fields @@ -92,7 +97,11 @@ class Service < ActiveRecord::Base end def event_names - supported_events.map { |event| "#{event}_events" } + self.class.event_names + end + + def self.event_names + self.supported_events.map { |event| "#{event}_events" } end def event_field(event) @@ -104,6 +113,10 @@ class Service < ActiveRecord::Base end def supported_events + self.class.supported_events + end + + def self.supported_events %w(push tag_push issue confidential_issue merge_request wiki_page) end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 98ccf5f331f..771a7350556 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -64,11 +64,11 @@ class Snippet < ActiveRecord::Base @link_reference_pattern ||= super("snippets", /(?<snippet>\d+)/) end - def to_reference(from_project = nil) + def to_reference(from_project = nil, full: false) reference = "#{self.class.reference_prefix}#{id}" if project.present? - "#{project.to_reference(from_project)}#{reference}" + "#{project.to_reference(from_project, full: full)}#{reference}" else reference end diff --git a/app/models/timelog.rb b/app/models/timelog.rb new file mode 100644 index 00000000000..f768c4e3da5 --- /dev/null +++ b/app/models/timelog.rb @@ -0,0 +1,6 @@ +class Timelog < ActiveRecord::Base + validates :time_spent, :user, presence: true + + belongs_to :trackable, polymorphic: true + belongs_to :user +end diff --git a/app/models/todo.rb b/app/models/todo.rb index f5ade1cc293..4c99aa0d3be 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -6,13 +6,15 @@ class Todo < ActiveRecord::Base BUILD_FAILED = 3 MARKED = 4 APPROVAL_REQUIRED = 5 # This is an EE-only feature + UNMERGEABLE = 6 ACTION_NAMES = { ASSIGNED => :assigned, MENTIONED => :mentioned, BUILD_FAILED => :build_failed, MARKED => :marked, - APPROVAL_REQUIRED => :approval_required + APPROVAL_REQUIRED => :approval_required, + UNMERGEABLE => :unmergeable } belongs_to :author, class_name: "User" @@ -66,6 +68,10 @@ class Todo < ActiveRecord::Base end end + def unmergeable? + action == UNMERGEABLE + end + def build_failed? action == BUILD_FAILED end diff --git a/app/models/user.rb b/app/models/user.rb index 899a89a4eaa..54f5388eb2c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -73,7 +73,7 @@ class User < ActiveRecord::Base has_many :created_projects, foreign_key: :creator_id, class_name: 'Project' has_many :users_star_projects, dependent: :destroy has_many :starred_projects, through: :users_star_projects, source: :project - has_many :project_authorizations, dependent: :destroy + has_many :project_authorizations has_many :authorized_projects, through: :project_authorizations, source: :project has_many :snippets, dependent: :destroy, foreign_key: :author_id @@ -99,6 +99,7 @@ class User < ActiveRecord::Base # # Note: devise :validatable above adds validations for :email and :password validates :name, presence: true + validates_confirmation_of :email validates :notification_email, presence: true validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true @@ -178,8 +179,8 @@ class User < ActiveRecord::Base scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all } scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') } scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) } - scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) } - scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) } + scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) } + scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) } def self.with_two_factor joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id"). @@ -332,7 +333,7 @@ class User < ActiveRecord::Base username end - def to_reference(_from_project = nil, _target_project = nil) + def to_reference(_from_project = nil, target_project: nil, full: nil) "#{self.class.reference_prefix}#{username}" end @@ -438,12 +439,21 @@ class User < ActiveRecord::Base Group.where("namespaces.id IN (#{union.to_sql})") end + def nested_groups + Group.member_descendants(id) + end + + def nested_projects + Project.joins(:namespace).where('namespaces.parent_id IS NOT NULL'). + member_descendants(id) + end + def refresh_authorized_projects Users::RefreshAuthorizedProjectsService.new(self).execute end def remove_project_authorizations(project_ids) - project_authorizations.where(id: project_ids).delete_all + project_authorizations.where(project_id: project_ids).delete_all end def set_authorized_projects_column diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 118c100ca11..b9f1c29c32e 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -53,6 +53,10 @@ class BasePolicy def self.class_for(subject) return GlobalPolicy if subject.nil? + if subject.class.try(:presenter?) + subject = subject.subject + end + subject.class.ancestors.each do |klass| next unless klass.name diff --git a/app/presenters/README.md b/app/presenters/README.md new file mode 100644 index 00000000000..a4d592b54d6 --- /dev/null +++ b/app/presenters/README.md @@ -0,0 +1,154 @@ +# Presenters + +This type of class is responsible for giving the view an object which defines +**view-related logic/data methods**. It is usually useful to extract such +methods from models to presenters. + +## When to use a presenter? + +### When your view is full of logic + +When your view is full of logic (`if`, `else`, `select` on arrays etc.), it's +time to create a presenter! + +### When your model has a lot of view-related logic/data methods + +When your model has a lot of view-related logic/data methods, you can easily +move them to a presenter. + +## Why are we using presenters instead of helpers? + +We don't use presenters to generate complex view output that would rely on helpers. + +Presenters should be used for: + +- Data and logic methods that can be pulled & combined into single methods from + view. This can include loops extracted from views too. A good example is + https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7073/diffs. +- Data and logic methods that can be pulled from models. +- Simple text output methods: it's ok if the method returns a string, but not a + whole DOM element for which we'd need HAML, a view context, helpers etc. + +## Why use presenters instead of model concerns? + +We should strive to follow the single-responsibility principle, and view-related +logic/data methods are definitely not the responsibility of models! + +Another reason is as follows: + +> Avoid using concerns and use presenters instead. Why? After all, concerns seem +to be a core part of Rails and can DRY up code when shared among multiple models. +Nonetheless, the main issue is that concerns don’t make the model object more +cohesive. The code is just better organized. In other words, there’s no real +change to the API of the model. + +– https://www.toptal.com/ruby-on-rails/decoupling-rails-components + +## Benefits + +By moving pure view-related logic/data methods from models & views to presenters, +we gain the following benefits: + +- rules are more explicit and centralized in the presenter => improves security +- testing is easier and faster as presenters are Plain Old Ruby Object (PORO) +- views are more readable and maintainable +- decreases number of CE -> EE merge conflicts since code is in separate files +- moves the conflicts from views (not always obvious) to presenters (a lot easier to resolve) + +## What not to do with presenters? + +- Don't use helpers in presenters. Presenters are not aware of the view context. +- Don't generate complex DOM elements, forms etc. with presenters. Presenters + can return simple data as texts, and URLs using URL helpers from + `Gitlab::Routing` but nothing much more fancy. + +## Implementation + +### Presenter definition + +Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which +provides a `.presents` method which allows you to define an accessor for the +presented object. It also includes common helpers like `Gitlab::Routing` and +`Gitlab::Allowable`. + +```ruby +class LabelPresenter < Gitlab::View::Presenter::Simple + presents :label + + def text_color + label.color.to_s + end + + def to_partial_path + 'projects/labels/show' + end +end +``` + +In some cases, it can be more practical to transparently delegate all missing +method calls to the presented object, in these cases, you can make your +presenter inherit from `Gitlab::View::Presenter::Delegated`: + +```ruby +class LabelPresenter < Gitlab::View::Presenter::Delegated + presents :label + + def text_color + # color is delegated to label + color.to_s + end + + def to_partial_path + 'projects/labels/show' + end +end +``` + +### Presenter instantiation + +Instantiation must be done via the `Gitlab::View::Presenter::Factory` class which +detects the presenter based on the presented subject's class. + +```ruby +class Projects::LabelsController < Projects::ApplicationController + def edit + @label = Gitlab::View::Presenter::Factory + .new(@label, current_user: current_user) + .fabricate! + end +end +``` + +You can also include the `Presentable` concern in the model: + +```ruby +class Label + include Presentable +end +``` + +and then in the controller: + +```ruby +class Projects::LabelsController < Projects::ApplicationController + def edit + @label = @label.present(current_user: current_user) + end +end +``` + +### Presenter usage + +```ruby +%div{ class: @label.text_color } + = render partial: @label, label: @label +``` + +You can also present the model in the view: + +```ruby +- label = @label.present(current_user: current_user) + +%div{ class: label.text_color } + = render partial: label, label: label +``` diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb new file mode 100644 index 00000000000..ed72ed14d72 --- /dev/null +++ b/app/presenters/ci/build_presenter.rb @@ -0,0 +1,15 @@ +module Ci + class BuildPresenter < Gitlab::View::Presenter::Delegated + presents :build + + def erased_by_user? + # Build can be erased through API, therefore it does not have + # `erased_by` user assigned in that case. + erased? && erased_by + end + + def erased_by_name + erased_by.name if erased_by_user? + end + end +end diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb new file mode 100644 index 00000000000..a559d0850c4 --- /dev/null +++ b/app/serializers/analytics_stage_entity.rb @@ -0,0 +1,10 @@ +class AnalyticsStageEntity < Grape::Entity + include EntityDateHelper + + expose :title + expose :description + + expose :median, as: :value do |stage| + stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil + end +end diff --git a/app/serializers/analytics_stage_serializer.rb b/app/serializers/analytics_stage_serializer.rb new file mode 100644 index 00000000000..613cf6874d8 --- /dev/null +++ b/app/serializers/analytics_stage_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsStageSerializer < BaseSerializer + entity AnalyticsStageEntity +end diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb new file mode 100644 index 00000000000..91803ec07f5 --- /dev/null +++ b/app/serializers/analytics_summary_entity.rb @@ -0,0 +1,7 @@ +class AnalyticsSummaryEntity < Grape::Entity + expose :value, safe: true + + expose :title do |object| + object.title.pluralize(object.value) + end +end diff --git a/app/serializers/analytics_summary_serializer.rb b/app/serializers/analytics_summary_serializer.rb new file mode 100644 index 00000000000..c87a24aa47c --- /dev/null +++ b/app/serializers/analytics_summary_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsSummarySerializer < BaseSerializer + entity AnalyticsSummaryEntity +end diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb new file mode 100644 index 00000000000..184f5fd4b52 --- /dev/null +++ b/app/serializers/build_action_entity.rb @@ -0,0 +1,14 @@ +class BuildActionEntity < Grape::Entity + include RequestAwareEntity + + expose :name do |build| + build.name + end + + expose :path do |build| + play_namespace_project_build_path( + build.project.namespace, + build.project, + build) + end +end diff --git a/app/serializers/build_artifact_entity.rb b/app/serializers/build_artifact_entity.rb new file mode 100644 index 00000000000..8b643d8e783 --- /dev/null +++ b/app/serializers/build_artifact_entity.rb @@ -0,0 +1,14 @@ +class BuildArtifactEntity < Grape::Entity + include RequestAwareEntity + + expose :name do |build| + build.name + end + + expose :path do |build| + download_namespace_project_build_artifacts_path( + build.project.namespace, + build.project, + build) + end +end diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb index acc20f6dc52..31763955f97 100644 --- a/app/serializers/commit_entity.rb +++ b/app/serializers/commit_entity.rb @@ -3,17 +3,21 @@ class CommitEntity < API::Entities::RepoCommit expose :author, using: UserEntity + expose :author_gravatar_url do |commit| + GravatarService.new.execute(commit.author_email) + end + expose :commit_url do |commit| - namespace_project_tree_url( + namespace_project_commit_url( request.project.namespace, request.project, - id: commit.id) + commit) end expose :commit_path do |commit| - namespace_project_tree_path( + namespace_project_commit_path( request.project.namespace, request.project, - id: commit.id) + commit) end end diff --git a/app/serializers/issuable_entity.rb b/app/serializers/issuable_entity.rb index 17c9160cb19..29aecb50849 100644 --- a/app/serializers/issuable_entity.rb +++ b/app/serializers/issuable_entity.rb @@ -13,4 +13,8 @@ class IssuableEntity < Grape::Entity expose :created_at expose :updated_at expose :deleted_at + expose :time_estimate + expose :total_time_spent + expose :human_time_estimate + expose :human_total_time_spent end diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb new file mode 100644 index 00000000000..61f0f11d7d2 --- /dev/null +++ b/app/serializers/pipeline_entity.rb @@ -0,0 +1,85 @@ +class PipelineEntity < Grape::Entity + include RequestAwareEntity + + expose :id + expose :user, using: UserEntity + + expose :path do |pipeline| + namespace_project_pipeline_path( + pipeline.project.namespace, + pipeline.project, + pipeline) + end + + expose :details do + expose :status do |pipeline, options| + StatusEntity.represent( + pipeline.detailed_status(request.user), + options) + end + + expose :duration + expose :finished_at + expose :stages, using: StageEntity + expose :artifacts, using: BuildArtifactEntity + expose :manual_actions, using: BuildActionEntity + end + + expose :flags do + expose :latest?, as: :latest + expose :triggered?, as: :triggered + expose :stuck?, as: :stuck + expose :has_yaml_errors?, as: :yaml_errors + expose :can_retry?, as: :retryable + expose :can_cancel?, as: :cancelable + end + + expose :ref do + expose :name do |pipeline| + pipeline.ref + end + + expose :path do |pipeline| + if pipeline.ref + namespace_project_tree_path( + pipeline.project.namespace, + pipeline.project, + id: pipeline.ref) + end + end + + expose :tag?, as: :tag + expose :branch?, as: :branch + end + + expose :commit, using: CommitEntity + expose :yaml_errors, if: ->(pipeline, _) { pipeline.has_yaml_errors? } + + expose :retry_path, if: proc { can_retry? } do |pipeline| + retry_namespace_project_pipeline_path(pipeline.project.namespace, + pipeline.project, + pipeline.id) + end + + expose :cancel_path, if: proc { can_cancel? } do |pipeline| + cancel_namespace_project_pipeline_path(pipeline.project.namespace, + pipeline.project, + pipeline.id) + end + + expose :created_at, :updated_at + + private + + alias_method :pipeline, :object + + def can_retry? + pipeline.retryable? && + can?(request.user, :update_pipeline, pipeline) + end + + def can_cancel? + pipeline.cancelable? && + can?(request.user, :update_pipeline, pipeline) + end +end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb new file mode 100644 index 00000000000..cfa86cc2553 --- /dev/null +++ b/app/serializers/pipeline_serializer.rb @@ -0,0 +1,40 @@ +class PipelineSerializer < BaseSerializer + entity PipelineEntity + class InvalidResourceError < StandardError; end + include API::Helpers::Pagination + Struct.new('Pagination', :request, :response) + + def represent(resource, opts = {}) + if paginated? + raise InvalidResourceError unless resource.respond_to?(:page) + + super(paginate(resource.includes(project: :namespace)), opts) + else + super(resource, opts) + end + end + + def paginated? + defined?(@pagination) + end + + def with_pagination(request, response) + tap { @pagination = Struct::Pagination.new(request, response) } + end + + private + + # Methods needed by `API::Helpers::Pagination` + # + def params + @pagination.request.query_parameters + end + + def request + @pagination.request + end + + def header(header, value) + @pagination.response.headers[header] = value + end +end diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb index e159d750cb7..3039014aaaa 100644 --- a/app/serializers/request_aware_entity.rb +++ b/app/serializers/request_aware_entity.rb @@ -2,14 +2,11 @@ module RequestAwareEntity extend ActiveSupport::Concern included do - include Gitlab::Routing.url_helpers + include Gitlab::Routing + include Gitlab::Allowable end def request - @options.fetch(:request) - end - - def can?(object, action, subject) - Ability.allowed?(object, action, subject) + options.fetch(:request) end end diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb new file mode 100644 index 00000000000..7a047bdc712 --- /dev/null +++ b/app/serializers/stage_entity.rb @@ -0,0 +1,38 @@ +class StageEntity < Grape::Entity + include RequestAwareEntity + + expose :name + + expose :title do |stage| + "#{stage.name}: #{detailed_status.label}" + end + + expose :detailed_status, + as: :status, + with: StatusEntity + + expose :path do |stage| + namespace_project_pipeline_path( + stage.pipeline.project.namespace, + stage.pipeline.project, + stage.pipeline, + anchor: stage.name) + end + + expose :dropdown_path do |stage| + stage_namespace_project_pipeline_path( + stage.pipeline.project.namespace, + stage.pipeline.project, + stage.pipeline, + stage: stage.name, + format: :json) + end + + private + + alias_method :stage, :object + + def detailed_status + stage.detailed_status(request.user) + end +end diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb new file mode 100644 index 00000000000..47066bebfb1 --- /dev/null +++ b/app/serializers/status_entity.rb @@ -0,0 +1,8 @@ +class StatusEntity < Grape::Entity + include RequestAwareEntity + + expose :icon, :text, :label, :group + + expose :has_details?, as: :has_details + expose :details_path +end diff --git a/app/services/application_settings/base_service.rb b/app/services/application_settings/base_service.rb new file mode 100644 index 00000000000..2bcc7d7c08b --- /dev/null +++ b/app/services/application_settings/base_service.rb @@ -0,0 +1,7 @@ +module ApplicationSettings + class BaseService < ::BaseService + def initialize(application_setting, user, params = {}) + @application_setting, @current_user, @params = application_setting, user, params.dup + end + end +end diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb new file mode 100644 index 00000000000..61589a07250 --- /dev/null +++ b/app/services/application_settings/update_service.rb @@ -0,0 +1,7 @@ +module ApplicationSettings + class UpdateService < ApplicationSettings::BaseService + def execute + @application_setting.update(@params) + end + end +end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 74b5ebf372b..6f03bf2be13 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -2,48 +2,72 @@ module Ci # This class responsible for assigning # proper pending build to runner on runner API request class RegisterBuildService - def execute(current_runner) - builds = Ci::Build.pending.unstarted + include Gitlab::CurrentSettings + attr_reader :runner + + Result = Struct.new(:build, :valid?) + + def initialize(runner) + @runner = runner + end + + def execute builds = - if current_runner.shared? - builds. - # don't run projects which have not enabled shared runners and builds - joins(:project).where(projects: { shared_runners_enabled: true }). - joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id'). - - # this returns builds that are ordered by number of running builds - # we prefer projects that don't use shared runners at all - joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id"). - where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). - order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') + if runner.shared? + builds_for_shared_runner else - # do run projects which are only assigned to this runner (FIFO) - builds.where(project: current_runner.projects.with_builds_enabled).order('created_at ASC') + builds_for_specific_runner end build = builds.find do |build| - current_runner.can_pick?(build) + runner.can_pick?(build) end if build # In case when 2 runners try to assign the same build, second runner will be declined # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. - build.runner_id = current_runner.id + build.runner_id = runner.id build.run! end - build + Result.new(build, true) rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError - nil + Result.new(build, false) end private + def builds_for_shared_runner + new_builds. + # don't run projects which have not enabled shared runners and builds + joins(:project).where(projects: { shared_runners_enabled: true }). + joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id'). + where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). + + # Implement fair scheduling + # this returns builds that are ordered by number of running builds + # we prefer projects that don't use shared runners at all + joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id"). + order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') + end + + def builds_for_specific_runner + new_builds.where(project: runner.projects.with_builds_enabled).order('created_at ASC') + end + def running_builds_for_shared_runners Ci::Build.running.where(runner: Ci::Runner.shared). group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds') end + + def new_builds + Ci::Build.pending.unstarted + end + + def shared_runner_build_limits_feature_enabled? + ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true' + end end end diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb new file mode 100644 index 00000000000..152c8ae5006 --- /dev/null +++ b/app/services/ci/update_build_queue_service.rb @@ -0,0 +1,19 @@ +module Ci + class UpdateBuildQueueService + def execute(build) + build.project.runners.each do |runner| + if runner.can_pick?(build) + runner.tick_runner_queue + end + end + + return unless build.project.shared_runners_enabled? + + Ci::Runner.shared.each do |runner| + if runner.can_pick?(build) + runner.tick_runner_queue + end + end + end + end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 4ce5fd993d9..5f3ced49665 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -36,6 +36,14 @@ class IssuableBaseService < BaseService end end + def create_time_estimate_note(issuable) + SystemNoteService.change_time_estimate(issuable, issuable.project, current_user) + end + + def create_time_spent_note(issuable) + SystemNoteService.change_time_spent(issuable, issuable.project, current_user) + end + def filter_params(issuable) ability_name = :"admin_#{issuable.to_ability_name}" @@ -272,6 +280,14 @@ class IssuableBaseService < BaseService create_task_status_note(issuable) end + if issuable.previous_changes.include?('time_estimate') + create_time_estimate_note(issuable) + end + + if issuable.time_spent? + create_time_spent_note(issuable) + end + create_labels_note(issuable, old_labels) if issuable.labels != old_labels end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 70e25956dc7..5a53b973059 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -38,15 +38,13 @@ module MergeRequests private - def merge_requests_for(branch) - origin_merge_requests = @project.origin_merge_requests - .opened.where(source_branch: branch).to_a - - fork_merge_requests = @project.fork_merge_requests - .opened.where(source_branch: branch).to_a - - (origin_merge_requests + fork_merge_requests) - .uniq.select(&:source_project) + # Returns all origin and fork merge requests from `@project` satisfying passed arguments. + def merge_requests_for(source_branch, mr_states: [:opened]) + MergeRequest + .with_state(mr_states) + .where(source_branch: source_branch, source_project_id: @project.id) + .preload(:source_project) # we don't need a #includes since we're just preloading for the #select + .select(&:source_project) end def pipeline_merge_requests(pipeline) diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 0a9563ed7e7..b4bfb0e5e8c 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -21,6 +21,7 @@ module MergeRequests end comment_mr_with_commits + mark_mr_as_wip_from_commits execute_mr_web_hooks true @@ -41,7 +42,7 @@ module MergeRequests commit_ids.include?(merge_request.diff_head_sha) end - merge_requests.uniq.select(&:source_project).each do |merge_request| + filter_merge_requests(merge_requests).each do |merge_request| MergeRequests::PostMergeService. new(merge_request.target_project, @current_user). execute(merge_request) @@ -57,10 +58,13 @@ module MergeRequests def reload_merge_requests merge_requests = @project.merge_requests.opened. by_source_or_target_branch(@branch_name).to_a - merge_requests += fork_merge_requests - merge_requests = filter_merge_requests(merge_requests) - merge_requests.each do |merge_request| + # Fork merge requests + merge_requests += MergeRequest.opened + .where(source_branch: @branch_name, source_project: @project) + .where.not(target_project: @project).to_a + + filter_merge_requests(merge_requests).each do |merge_request| if merge_request.source_branch == @branch_name || force_push? merge_request.reload_diff else @@ -136,6 +140,24 @@ module MergeRequests end end + def mark_mr_as_wip_from_commits + return unless @commits.present? + + merge_requests_for_source_branch.each do |merge_request| + wip_commit = @commits.detect(&:work_in_progress?) + + if wip_commit && !merge_request.work_in_progress? + merge_request.update(title: merge_request.wip_title) + SystemNoteService.add_merge_request_wip_from_commit( + merge_request, + merge_request.project, + @current_user, + wip_commit + ) + end + end + end + # Call merge request webhook with update branches def execute_mr_web_hooks merge_requests_for_source_branch.each do |merge_request| @@ -156,16 +178,7 @@ module MergeRequests end def merge_requests_for_source_branch - @source_merge_requests ||= begin - merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a - merge_requests += fork_merge_requests - filter_merge_requests(merge_requests) - end - end - - def fork_merge_requests - @fork_merge_requests ||= @project.fork_merge_requests.opened. - where(source_branch: @branch_name).to_a + @source_merge_requests ||= merge_requests_for(@branch_name) end def branch_added? diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb index ad16ef8c70f..3cb9aae83f6 100644 --- a/app/services/merge_requests/update_service.rb +++ b/app/services/merge_requests/update_service.rb @@ -7,6 +7,8 @@ module MergeRequests params.except!(:target_project_id) params.except!(:source_branch) + merge_from_slash_command(merge_request) if params[:merge] + if merge_request.closed_without_fork? params.except!(:target_branch, :force_remove_source_branch) end @@ -69,6 +71,19 @@ module MergeRequests end end + def merge_from_slash_command(merge_request) + last_diff_sha = params.delete(:merge) + return unless merge_request.mergeable_with_slash_command?(current_user, last_diff_sha: last_diff_sha) + + merge_request.update(merge_error: nil) + + if merge_request.head_pipeline && merge_request.head_pipeline.active? + MergeRequests::MergeWhenPipelineSucceedsService.new(project, current_user).execute(merge_request) + else + MergeWorker.perform_async(merge_request.id, current_user.id, {}) + end + end + def reopen_service MergeRequests::ReopenService end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index 1beca9f4109..b4f8b33d564 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -1,9 +1,12 @@ module Notes class CreateService < BaseService def execute - note = project.notes.new(params) - note.author = current_user - note.system = false + merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha) + + note = Note.new(params) + note.project = project + note.author = current_user + note.system = false if note.award_emoji? noteable = note.noteable @@ -19,7 +22,8 @@ module Notes slash_commands_service = SlashCommandsService.new(project, current_user) if slash_commands_service.supported?(note) - content, command_params = slash_commands_service.extract_commands(note) + options = { merge_request_diff_head_sha: merge_request_diff_head_sha } + content, command_params = slash_commands_service.extract_commands(note, options) only_commands = content.empty? diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index e4cd3fc7833..6a10e172483 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -10,6 +10,9 @@ module Notes # Skip system notes, like status changes and cross-references and awards unless @note.system? EventCreateService.new.leave_note(@note, @note.author) + + return if @note.for_personal_snippet? + @note.create_cross_references! execute_note_hooks end diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb index 2edbd39a9e7..56913568cae 100644 --- a/app/services/notes/slash_commands_service.rb +++ b/app/services/notes/slash_commands_service.rb @@ -12,17 +12,17 @@ module Notes def self.supported?(note, current_user) noteable_update_service(note) && current_user && - current_user.can?(:"update_#{note.noteable_type.underscore}", note.noteable) + current_user.can?(:"update_#{note.to_ability_name}", note.noteable) end def supported?(note) self.class.supported?(note, current_user) end - def extract_commands(note) + def extract_commands(note, options = {}) return [note.note, {}] unless supported?(note) - SlashCommands::InterpretService.new(project, current_user). + SlashCommands::InterpretService.new(project, current_user, options). execute(note.note, note.noteable) end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 9a7af5730d2..f74e6cac174 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -178,8 +178,15 @@ class NotificationService recipients = [] mentioned_users = note.mentioned_users + + ability, subject = if note.for_personal_snippet? + [:read_personal_snippet, note.noteable] + else + [:read_project, note.project] + end + mentioned_users.select! do |user| - user.can?(:read_project, note.project) + user.can?(ability, subject) end # Add all users participating in the thread (author, assignee, comment authors) @@ -192,11 +199,13 @@ class NotificationService recipients = recipients.concat(participants) - # Merge project watchers - recipients = add_project_watchers(recipients, note.project) + unless note.for_personal_snippet? + # Merge project watchers + recipients = add_project_watchers(recipients, note.project) - # Merge project with custom notification - recipients = add_custom_notifications(recipients, note.project, :new_note) + # Merge project with custom notification + recipients = add_custom_notifications(recipients, note.project, :new_note) + end # Reject users with Mention notification level, except those mentioned in _this_ note. recipients = reject_mention_users(recipients - mentioned_users, note.project) @@ -211,8 +220,7 @@ class NotificationService recipients.delete(note.author) recipients = recipients.uniq - # build notify method like 'note_commit_email' - notify_method = "note_#{note.noteable_type.underscore}_email".to_sym + notify_method = "note_#{note.to_ability_name}_email".to_sym recipients.each do |recipient| mailer.send(notify_method, recipient.id, note.id).deliver_later @@ -591,7 +599,10 @@ class NotificationService custom_action = build_custom_key(action, target) recipients = target.participants(current_user) - recipients = add_project_watchers(recipients, project) + + unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action) + recipients = add_project_watchers(recipients, project) + end recipients = add_custom_notifications(recipients, project, custom_action) recipients = reject_mention_users(recipients, project) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 159f46cd465..c7cce0c55b9 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -22,17 +22,7 @@ module Projects return @project end - # Set project name from path - if @project.name.present? && @project.path.present? - # if both name and path set - everything is ok - elsif @project.path.present? - # Set project name from path - @project.name = @project.path.dup - elsif @project.name.present? - # For compatibility - set path from name - # TODO: remove this in 8.0 - @project.path = @project.name.dup.parameterize - end + set_project_name_from_path # get namespace id namespace_id = params[:namespace_id] @@ -144,5 +134,19 @@ module Projects service.save! end end + + def set_project_name_from_path + # Set project name from path + if @project.name.present? && @project.path.present? + # if both name and path set - everything is ok + elsif @project.path.present? + # Set project name from path + @project.name = @project.path.dup + elsif @project.name.present? + # For compatibility - set path from name + # TODO: remove this in 8.0 + @project.path = @project.name.dup.parameterize + end + end end end diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb index 6040391fd94..96c363c8d1a 100644 --- a/app/services/projects/participants_service.rb +++ b/app/services/projects/participants_service.rb @@ -36,7 +36,7 @@ module Projects def groups current_user.authorized_groups.sort_by(&:path).map do |group| count = group.users.count - { username: group.path, name: group.name, count: count, avatar_url: group.avatar.url } + { username: group.path, name: group.name, count: count, avatar_url: group.avatar_url } end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 8a6af8d8ada..55d9cb13ae4 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -9,7 +9,7 @@ module Projects Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility) deny_visibility_level(project, new_visibility) - return project + return error('Visibility level unallowed') end end @@ -22,7 +22,13 @@ module Projects if project.update_attributes(params.except(:default_branch)) if project.previous_changes.include?('path') project.rename_repo + else + system_hook_service.execute_hooks_for(project, :update) end + + success + else + error('Project could not be updated') end end end diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index d75c5b1800e..3566a8ba92f 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -2,7 +2,7 @@ module SlashCommands class InterpretService < BaseService include Gitlab::SlashCommands::Dsl - attr_reader :issuable + attr_reader :issuable, :options # Takes a text and interprets the commands that are extracted from it. # Returns the content without commands, and hash of changes to be applied to a record. @@ -13,7 +13,8 @@ module SlashCommands opts = { issuable: issuable, current_user: current_user, - project: project + project: project, + params: params } content, commands = extractor.extract_commands(content, opts) @@ -58,6 +59,17 @@ module SlashCommands @updates[:state_event] = 'reopen' end + desc 'Merge (when build succeeds)' + condition do + last_diff_sha = params && params[:merge_request_diff_head_sha] + issuable.is_a?(MergeRequest) && + issuable.persisted? && + issuable.mergeable_with_slash_command?(current_user, autocomplete_precheck: !last_diff_sha, last_diff_sha: last_diff_sha) + end + command :merge do + @updates[:merge] = params[:merge_request_diff_head_sha] + end + desc 'Change title' params '<New title>' condition do @@ -243,6 +255,50 @@ module SlashCommands @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip' end + desc 'Set time estimate' + params '<1w 3d 2h 14m>' + condition do + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + command :estimate do |raw_duration| + time_estimate = Gitlab::TimeTrackingFormatter.parse(raw_duration) + + if time_estimate + @updates[:time_estimate] = time_estimate + end + end + + desc 'Add or substract spent time' + params '<1h 30m | -1h 30m>' + condition do + current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) + end + command :spend do |raw_duration| + time_spent = Gitlab::TimeTrackingFormatter.parse(raw_duration) + + if time_spent + @updates[:spend_time] = { duration: time_spent, user: current_user } + end + end + + desc 'Remove time estimate' + condition do + issuable.persisted? && + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + command :remove_estimate do + @updates[:time_estimate] = 0 + end + + desc 'Remove spent time' + condition do + issuable.persisted? && + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + command :remove_time_spent do + @updates[:spend_time] = { duration: :reset, user: current_user } + end + # This is a dummy command, so that it appears in the autocomplete commands desc 'CC' params '@user' diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 7613ecd5021..a11bca00687 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -109,6 +109,57 @@ module SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when the estimated time of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # time_estimate - Estimated time + # + # Example Note text: + # + # "Changed estimate of this issue to 3d 5h" + # + # Returns the created Note object + + def change_time_estimate(noteable, project, author) + parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate) + body = if noteable.time_estimate == 0 + "Removed time estimate on this #{noteable.human_class_name}" + else + "Changed time estimate of this #{noteable.human_class_name} to #{parsed_time}" + end + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when the spent time of a Noteable is changed + # + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # time_spent - Spent time + # + # Example Note text: + # + # "Added 2h 30m of time spent on this issue" + # + # Returns the created Note object + + def change_time_spent(noteable, project, author) + time_spent = noteable.time_spent + + if time_spent == :reset + body = "Removed time spent on this #{noteable.human_class_name}" + else + parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs) + action = time_spent > 0 ? 'Added' : 'Subtracted' + body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}" + end + + create_note(noteable: noteable, project: project, author: author, note: body) + end + # Called when the status of a Noteable is changed # # noteable - Noteable object @@ -157,6 +208,12 @@ module SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + def add_merge_request_wip_from_commit(noteable, project, author, commit) + body = "marked as a **Work In Progress** from #{commit.to_reference(project)}" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + def self.resolve_all_discussions(merge_request, project, author) body = "resolved all discussions" diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index f8e6b2ef094..1bd6ce416ab 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -98,10 +98,12 @@ class TodoService # When a build fails on the HEAD of a merge request we should: # - # * create a todo for that user to fix it + # * create a todo for author of MR to fix it + # * create a todo for merge_user to keep an eye on it # def merge_request_build_failed(merge_request) - create_build_failed_todo(merge_request) + create_build_failed_todo(merge_request, merge_request.author) + create_build_failed_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds? end # When a new commit is pushed to a merge request we should: @@ -115,11 +117,21 @@ class TodoService # When a build is retried to a merge request we should: # # * mark all pending todos related to the merge request for the author as done + # * mark all pending todos related to the merge request for the merge_user as done # def merge_request_build_retried(merge_request) mark_pending_todos_as_done(merge_request, merge_request.author) + mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds? end - + + # When a merge request could not be automatically merged due to its unmergeable state we should: + # + # * create a todo for a merge_user + # + def merge_request_became_unmergeable(merge_request) + create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_build_succeeds? + end + # When create a note we should: # # * mark all pending todos related to the noteable for the note author as done @@ -236,10 +248,14 @@ class TodoService create_todos(mentioned_users, attributes) end - def create_build_failed_todo(merge_request) - author = merge_request.author - attributes = attributes_for_todo(merge_request.project, merge_request, author, Todo::BUILD_FAILED) - create_todos(author, attributes) + def create_build_failed_todo(merge_request, todo_author) + attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::BUILD_FAILED) + create_todos(todo_author, attributes) + end + + def create_unmergeable_todo(merge_request, merge_user) + attributes = attributes_for_todo(merge_request.project, merge_request, merge_user, Todo::UNMERGEABLE) + create_todos(merge_user, attributes) end def attributes_for_target(target) diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb index 2469b4f0d7c..d7a6804ee88 100644 --- a/app/services/user_project_access_changed_service.rb +++ b/app/services/user_project_access_changed_service.rb @@ -4,6 +4,6 @@ class UserProjectAccessChangedService end def execute - AuthorizedProjectsWorker.bulk_perform_async(@user_ids.map { |id| [id] }) + AuthorizedProjectsWorker.bulk_perform_and_wait(@user_ids.map { |id| [id] }) end end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index 8559908e0c3..fad741531ea 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -26,8 +26,26 @@ module Users user.reload end - # This method returns the updated User object. def execute + lease_key = "refresh_authorized_projects:#{user.id}" + lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) + + until uuid = lease.try_obtain + # Keep trying until we obtain the lease. If we don't do so we may end up + # not updating the list of authorized projects properly. To prevent + # hammering Redis too much we'll wait for a bit between retries. + sleep(1) + end + + begin + execute_without_lease + ensure + Gitlab::ExclusiveLease.cancel(lease_key, uuid) + end + end + + # This method returns the updated User object. + def execute_without_lease current = current_authorizations_per_project fresh = fresh_access_levels_per_project @@ -35,7 +53,7 @@ module Users # rows not in the new list or with a different access level should be # removed. if !fresh[project_id] || fresh[project_id] != row.access_level - array << row.id + array << row.project_id end end @@ -47,26 +65,7 @@ module Users end end - update_with_lease(remove, add) - end - - # Updates the list of authorizations using an exclusive lease. - def update_with_lease(remove = [], add = []) - lease_key = "refresh_authorized_projects:#{user.id}" - lease = Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) - - until uuid = lease.try_obtain - # Keep trying until we obtain the lease. If we don't do so we may end up - # not updating the list of authorized projects properly. To prevent - # hammering Redis too much we'll wait for a bit between retries. - sleep(1) - end - - begin - update_authorizations(remove, add) - ensure - Gitlab::ExclusiveLease.cancel(lease_key, uuid) - end + update_authorizations(remove, add) end # Updates the list of authorizations for the current user. @@ -100,7 +99,7 @@ module Users end def current_authorizations - user.project_authorizations.select(:id, :project_id, :access_level) + user.project_authorizations.select(:project_id, :access_level) end def fresh_authorizations @@ -119,7 +118,8 @@ module Users user.personal_projects.select("#{user.id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"), user.groups_projects.select_for_project_authorization, user.projects.select_for_project_authorization, - user.groups.joins(:shared_projects).select_for_project_authorization + user.groups.joins(:shared_projects).select_for_project_authorization, + user.nested_projects.select_for_project_authorization ] Gitlab::SQL::Union.new(relations) diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index a1ecb7bc00b..265cea2d2c6 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -10,4 +10,15 @@ class AvatarUploader < GitlabUploader def exists? model.avatar.file && model.avatar.file.exists? end + + # We set move_to_store and move_to_cache to 'false' to prevent stealing + # the avatar file from a project when forking it. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158 + def move_to_store + false + end + + def move_to_cache + false + end end diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb index aec0d0ce44e..36279daa743 100644 --- a/app/validators/project_path_validator.rb +++ b/app/validators/project_path_validator.rb @@ -15,7 +15,7 @@ class ProjectPathValidator < ActiveModel::EachValidator # 'tree' as project name and 'deploy_keys' as route. # RESERVED = (NamespaceValidator::RESERVED - - %w[dashboard help ci admin search notes services] + + %w[dashboard help ci admin search notes services assets profile public] + %w[tree commits wikis new edit create update logs_tree preview blob blame raw files create_dir find_file]).freeze diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 4612a7a058a..558bbe07b16 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -421,6 +421,23 @@ = link_to "Koding administration documentation", help_page_path("administration/integration/koding") %fieldset + %legend PlantUML + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :plantuml_enabled do + = f.check_box :plantuml_enabled + Enable PlantUML + .form-group + = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' + .help-block + Allow rendering of + = link_to "PlantUML", "http://plantuml.com" + diagrams in Asciidoc documents using an external PlantUML service. + + %fieldset %legend Usage statistics .form-group .col-sm-offset-2.col-sm-10 diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 3132d157f29..2269fb1fd8c 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -4,7 +4,7 @@ - if @broadcast_message.message.present? = render_broadcast_message(@broadcast_message) - else - = "Your message here" + Your message here = form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f| = form_errors(@broadcast_message) diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml index 7362d904b94..8c658905bd6 100644 --- a/app/views/admin/identities/_identity.html.haml +++ b/app/views/admin/identities/_identity.html.haml @@ -1,6 +1,6 @@ %tr %td - = "#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})" + #{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider}) %td = identity.extern_uid %td diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 37bb6a3b0e0..124f970524e 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -11,7 +11,7 @@ that for future communication. %br Registration token is - %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token} + %code#runners-token= current_application_settings.runners_registration_token .bs-callout.clearfix .pull-left diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index ca503e35623..39e103e3062 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -100,7 +100,7 @@ %td.build-link - if project = link_to ci_status_path(build.pipeline) do - %strong #{build.pipeline.short_sha} + %strong= build.pipeline.short_sha %td.timestamp - if build.finished_at diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml index bfc6142067a..2e5f120c4e4 100644 --- a/app/views/admin/system_info/show.html.haml +++ b/app/views/admin/system_info/show.html.haml @@ -10,7 +10,7 @@ %h4 CPU .data - if @cpus - %h1= "#{@cpus.length} cores" + %h1 #{@cpus.length} cores - else = icon('warning', class: 'text-warning') Unable to collect CPU info @@ -19,7 +19,7 @@ %h4 Memory .data - if @memory - %h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}" + %h1 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)} - else = icon('warning', class: 'text-warning') Unable to collect memory info @@ -28,6 +28,6 @@ %h4 Disks .data - @disks.each do |disk| - %h1= "#{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])}" - %p= "#{disk[:disk_name]}" - %p= "#{disk[:mount_path]}" + %h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])} + %p= disk[:disk_name] + %p= disk[:mount_path] diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index a71240986c9..76b1291fe10 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -186,6 +186,6 @@ - if @user.solo_owned_groups.present? %p This user is currently an owner in these groups: - %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %strong= @user.solo_owned_groups.map(&:name).join(', ') %p You must transfer ownership or delete these groups before you can delete this user. diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index d8912eda314..e3305e21e96 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -2,8 +2,7 @@ .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", - disabled: !current_user, - class: (award_active_class(awards, current_user)), + class: (award_state_class(awards, current_user)), data: { placement: "bottom", title: award_user_list(awards, current_user) } } = emoji_icon(emoji, sprite: false) %span.award-control-text.js-counter diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml index 889086c62b1..b0bee1c6204 100644 --- a/app/views/ci/lints/show.html.haml +++ b/app/views/ci/lints/show.html.haml @@ -1,20 +1,25 @@ - page_title "CI Lint" - page_description "Validate your GitLab CI configuration file" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') %h2 Check your .gitlab-ci.yml -%hr -.row - = form_tag ci_lint_path, method: :post do - .form-group - = label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap') +.ci-linter + .row + = form_tag ci_lint_path, method: :post do + .form-group + .col-sm-12 + .file-holder + .file-title.clearfix + Content of .gitlab-ci.yml + #ci-editor.ci-editor= @content + = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true) .col-sm-12 - = text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true) - .col-sm-12 - .pull-left.prepend-top-10 - = submit_tag('Validate', class: 'btn btn-success submit-yml') + .pull-left.prepend-top-10 + = submit_tag('Validate', class: 'btn btn-success submit-yml') -.row.prepend-top-20 - .col-sm-12 - .results - = render partial: 'create' if defined?(@status) + .row.prepend-top-20 + .col-sm-12 + .results.ci-template + = render partial: 'create' if defined?(@status) diff --git a/app/views/ci/status/_badge.html.haml b/app/views/ci/status/_badge.html.haml index 601fb7f0f3f..c00c7f7407e 100644 --- a/app/views/ci/status/_badge.html.haml +++ b/app/views/ci/status/_badge.html.haml @@ -1,7 +1,8 @@ - status = local_assigns.fetch(:status) +- link = local_assigns.fetch(:link, true) - css_classes = "ci-status ci-#{status.group}" -- if status.has_details? +- if link && status.has_details? = link_to status.details_path, class: css_classes do = custom_icon(status.icon) = status.text diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml new file mode 100644 index 00000000000..8dea3479f82 --- /dev/null +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -0,0 +1,19 @@ +-# Renders the content of each li in the dropdown + +- subject = local_assigns.fetch(:subject) +- status = subject.detailed_status(current_user) +- klass = "ci-status-icon ci-status-icon-#{status.group}" +- tooltip = "#{subject.name} - #{status.label}" + +- if status.has_details? + = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip } do + %span{ class: klass }= custom_icon(status.icon) + %span.ci-build-text= subject.name +- else + .mini-pipeline-graph-dropdown-item{ data: { toggle: 'tooltip', title: tooltip } } + %span{ class: klass }= custom_icon(status.icon) + %span.ci-build-text= subject.name + +- if status.has_action? + = link_to status.action_path, class: 'ci-action-icon-wrapper js-ci-action-icon', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do + = icon(status.action_icon, class: status.action_class) diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml index 9849b31d7e2..9d7bcdb9d16 100644 --- a/app/views/dashboard/todos/_todo.html.haml +++ b/app/views/dashboard/todos/_todo.html.haml @@ -3,7 +3,7 @@ .todo-item.todo-block .todo-title.title - - unless todo.build_failed? + - unless todo.build_failed? || todo.unmergeable? = todo_target_state_pill(todo) %span.author-name diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 545a938f4be..01ecf237925 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -15,6 +15,9 @@ .form-group = f.label :email = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." + .form-group + = f.label :email_confirmation + = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address." .form-group.append-bottom-20#password-strength = f.label :password = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml index 2bce2780484..6f5d4bf2a2f 100644 --- a/app/views/discussions/_discussion.html.haml +++ b/app/views/discussions/_discussion.html.haml @@ -1,6 +1,9 @@ - expanded = discussion.expanded? %li.note.note-discussion.timeline-entry .timeline-entry-inner + .timeline-icon + = link_to user_path(discussion.author) do + = image_tag avatar_icon(discussion.author), class: "avatar s40" .timeline-content .discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } } .discussion-header diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml index ef16b516e2c..3a19e021643 100644 --- a/app/views/discussions/_parallel_diff_discussion.html.haml +++ b/app/views/discussions/_parallel_diff_discussion.html.haml @@ -6,7 +6,7 @@ .content{ class: ('hide' unless discussion_left.expanded?) } = render "discussions/notes", discussion: discussion_left, line_type: 'old' - else - %td.notes_line.old= "" + %td.notes_line.old= ("") %td.notes_content.parallel.old .content @@ -16,6 +16,6 @@ .content{ class: ('hide' unless discussion_right.expanded?) } = render "discussions/notes", discussion: discussion_right, line_type: 'new' - else - %td.notes_line.new= "" + %td.notes_line.new= ("") %td.notes_content.parallel.new .content diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index 2a0e301c8dd..a196561f381 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -10,7 +10,7 @@ %p = icon("exclamation-triangle fw") You are an admin, which means granting access to - %strong #{@pre_auth.client.name} + %strong= @pre_auth.client.name will allow them to interact with GitLab as an admin as well. Proceed with caution. - if @pre_auth.scopes diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index bc5d3c797ac..f4c432a095a 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -25,7 +25,7 @@ .panel.panel-default .panel-heading Users with access to - %strong #{@group.name} + %strong= @group.name %span.badge= @members.total_count %ul.content-list = render partial: 'shared/members/member', collection: @members, as: :member diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index b4aa4f24d9e..6ad03a60b3a 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -18,7 +18,7 @@ .row-content-block.second-block Only issues from the - %strong #{@group.name} + %strong= @group.name group are listed here. - if current_user To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index dbbdb583a24..af73554086b 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -10,7 +10,7 @@ .row-content-block.second-block Only merge requests from - %strong #{@group.name} + %strong= @group.name group are listed here. - if current_user To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index a8fdbd8c426..cd5388fe402 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -10,7 +10,7 @@ .row-content-block Only milestones from - %strong #{@group.name} + %strong= @group.name group are listed here. .milestones diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index d19eaa6add9..38d63fd9acc 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -21,5 +21,5 @@ = render 'shared/group_tips' .form-actions - = f.submit 'Create group', class: "btn btn-create", tabindex: 3 + = f.submit 'Create group', class: "btn btn-create" = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel' diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml index 864c5c0ff95..0e7f0b5ed4f 100644 --- a/app/views/import/_githubish_status.html.haml +++ b/app/views/import/_githubish_status.html.haml @@ -16,7 +16,7 @@ %colgroup.import-jobs-status-col %thead %tr - %th= "From #{provider_title}" + %th From #{provider_title} %th To GitLab %th Status %tbody diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index 07338736bac..9999a4362c6 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -37,7 +37,7 @@ %tbody - @user_map.each do |id, user| %tr - %td= id + %td= (id) %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control' %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control' %td diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml index 97e5e51abe0..5de5da5e6a2 100644 --- a/app/views/import/fogbugz/status.html.haml +++ b/app/views/import/fogbugz/status.html.haml @@ -50,7 +50,7 @@ %td = repo.name %td.import-target - = "#{current_user.username}/#{repo.name}" + #{current_user.username}/#{repo.name} %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do Import diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml index 9f1507cade6..5e01af008be 100644 --- a/app/views/import/google_code/status.html.haml +++ b/app/views/import/google_code/status.html.haml @@ -55,7 +55,7 @@ %td = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank" %td.import-target - = "#{current_user.username}/#{repo.name}" + #{current_user.username}/#{repo.name} %td.import-actions.job-status = button_tag class: "btn btn-import js-add-to-import" do Import diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index f4e0244596c..9ecc0d11c95 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -44,7 +44,7 @@ %li = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username } %li - = link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" } + = link_to "Settings", profile_path, aria: { label: "Settings" } %li = link_to "Help", help_path, aria: { label: "Help" } %li.divider diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index b69114c96cc..ac04f57e217 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -31,7 +31,7 @@ = link_to admin_abuse_reports_path, title: "Abuse Reports" do %span Abuse Reports - %span.badge.badge-dark.count= number_with_delimiter(AbuseReport.count(:all)) + %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) - if askimet_enabled? = nav_link(controller: :spam_logs) do diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index 221f3ec1ffe..f3539fd372d 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -26,13 +26,13 @@ %span Issues - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.badge-dark.count= number_with_delimiter(issues.count) + %span.badge.count= number_with_delimiter(issues.count) = nav_link(path: 'groups#merge_requests') do = link_to merge_requests_group_path(@group), title: 'Merge Requests' do %span Merge Requests - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - %span.badge.badge-dark.count= number_with_delimiter(merge_requests.count) + %span.badge.count= number_with_delimiter(merge_requests.count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do %span diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 3c8c7b8f25e..a8bbd67de80 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -61,14 +61,14 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.badge-dark.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.badge-dark.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :wiki = nav_link(controller: :wikis) do diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index 613b8b7d301..c6df66d2c3c 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -1,26 +1,17 @@ - if project_nav_tab? :team - = nav_link(controller: [:project_members, :teams]) do - = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do + = nav_link(controller: [:members, :teams]) do + = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do %span Members - if can_edit - - if @project.allowed_to_share_with_group? - = nav_link(controller: :group_links) do - = link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do - %span - Groups = nav_link(controller: :deploy_keys) do = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do %span Deploy Keys - = nav_link(controller: :hooks) do - = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Webhooks' do - %span - Webhooks - = nav_link(controller: :services) do - = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do + = nav_link(controller: :integrations) do + = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do %span - Services + Integrations = nav_link(controller: :protected_branches) do = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do %span diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml index b77d3402a2e..0ee8a57dbd4 100644 --- a/app/views/layouts/profile.html.haml +++ b/app/views/layouts/profile.html.haml @@ -1,5 +1,5 @@ -- page_title "Profile Settings" -- header_title "Profile Settings", profile_path unless header_title +- page_title "User Settings" +- header_title "User Settings", profile_path unless header_title - sidebar "dashboard" - nav "profile" diff --git a/app/views/notify/_reassigned_issuable_email.html.haml b/app/views/notify/_reassigned_issuable_email.html.haml index 56d81b2ed2e..fd35713f79c 100644 --- a/app/views/notify/_reassigned_issuable_email.html.haml +++ b/app/views/notify/_reassigned_issuable_email.html.haml @@ -2,9 +2,9 @@ Assignee changed - if @previous_assignee from - %strong #{@previous_assignee.name} + %strong= @previous_assignee.name to - if issuable.assignee_id - %strong #{issuable.assignee_name} + %strong= issuable.assignee_name - else %strong Unassigned diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml index 56c18cd83cd..b7284dd819b 100644 --- a/app/views/notify/closed_issue_email.html.haml +++ b/app/views/notify/closed_issue_email.html.haml @@ -1,2 +1,2 @@ %p - = "Issue was closed by #{@updated_by.name}" + Issue was closed by #{@updated_by.name} diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml index ac703b31edd..bc12e38675f 100644 --- a/app/views/notify/closed_issue_email.text.haml +++ b/app/views/notify/closed_issue_email.text.haml @@ -1,3 +1,3 @@ -= "Issue was closed by #{@updated_by.name}" +Issue was closed by #{@updated_by.name} Issue ##{@issue.iid}: #{namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)} diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml index 81c7c88fc96..44e018304e1 100644 --- a/app/views/notify/closed_merge_request_email.html.haml +++ b/app/views/notify/closed_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}" + Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name} diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index b435067d5a6..d0c96b83976 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}" +Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name} Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/issue_status_changed_email.html.haml b/app/views/notify/issue_status_changed_email.html.haml index 482c884a9db..b6051b11cea 100644 --- a/app/views/notify/issue_status_changed_email.html.haml +++ b/app/views/notify/issue_status_changed_email.html.haml @@ -1,2 +1,2 @@ %p - = "Issue was #{@issue_status} by #{@updated_by.name}" + Issue was #{@issue_status} by #{@updated_by.name} diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml index 41a320d6bd8..b487e26b122 100644 --- a/app/views/notify/merge_request_status_email.html.haml +++ b/app/views/notify/merge_request_status_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}" + Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name} diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml index 7a5074a1dc3..4c9719ba732 100644 --- a/app/views/notify/merge_request_status_email.text.haml +++ b/app/views/notify/merge_request_status_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}" +Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name} Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml index fbe506d4f4d..0fe54e73313 100644 --- a/app/views/notify/merged_merge_request_email.html.haml +++ b/app/views/notify/merged_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - = "Merge Request #{@merge_request.to_reference} was merged" + Merge Request #{@merge_request.to_reference} was merged diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index bfbae01094f..46c1c9dee0b 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -1,4 +1,4 @@ -= "Merge Request #{@merge_request.to_reference} was merged" +Merge Request #{@merge_request.to_reference} was merged Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)} diff --git a/app/views/notify/note_personal_snippet_email.html.haml b/app/views/notify/note_personal_snippet_email.html.haml new file mode 100644 index 00000000000..2fa2f784661 --- /dev/null +++ b/app/views/notify/note_personal_snippet_email.html.haml @@ -0,0 +1 @@ += render 'note_message' diff --git a/app/views/notify/note_personal_snippet_email.text.erb b/app/views/notify/note_personal_snippet_email.text.erb new file mode 100644 index 00000000000..b2a8809a23b --- /dev/null +++ b/app/views/notify/note_personal_snippet_email.text.erb @@ -0,0 +1,8 @@ +New comment for Snippet <%= @snippet.id %> + +<%= url_for(snippet_url(@snippet, anchor: "note_#{@note.id}")) %> + + +Author: <%= @note.author_name %> + +<%= @note.note %> diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index 82c7fe229b8..d9ebbaa2704 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -139,7 +139,7 @@ had = failed.size failed - = "#{'build'.pluralize(failed.size)}." + #{'build'.pluralize(failed.size)}. %tr.warning %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } Logs may contain sensitive data. Please consider before forwarding this email. diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 6dddb3b6373..8add2e18206 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -138,9 +138,9 @@ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" } = "\##{@pipeline.id}" successfully completed - = "#{build_count} #{'build'.pluralize(build_count)}" + #{build_count} #{'build'.pluralize(build_count)} in - = "#{stage_count} #{'stage'.pluralize(stage_count)}." + #{stage_count} #{'stage'.pluralize(stage_count)}. %tr.footer %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" } %img{ alt: "GitLab", height: "33", src: image_url('mailers/ci_pipeline_notif_v1/gitlab-logo-full-horizontal.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/ diff --git a/app/views/notify/project_was_not_exported_email.text.haml b/app/views/notify/project_was_not_exported_email.text.haml index 27785165c2d..6c6902994a1 100644 --- a/app/views/notify/project_was_not_exported_email.text.haml +++ b/app/views/notify/project_was_not_exported_email.text.haml @@ -1,6 +1,6 @@ -= "Project #{@project.name} couldn't be exported." +Project #{@project.name} couldn't be exported. -= "The errors we encountered were:" +The errors we encountered were: - @errors.each do |error| #{error} diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 36858fa6f34..c6b1db17f91 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -17,7 +17,7 @@ %ul - @message.commits.each do |commit| %li - %strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))} + %strong= link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit)) %div %span by #{commit.author_name} %i at #{commit.committed_date.to_s(:iso8601)} diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 72f658d1b68..14b330d16ad 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -102,7 +102,7 @@ = f.text_field :username, required: true, class: 'form-control' .help-block Current path: - = "#{root_url}#{current_user.username}" + #{root_url}#{current_user.username} .prepend-top-default = f.button class: "btn btn-warning", type: "submit" do = icon "spinner spin", class: "hidden loading-username" @@ -128,7 +128,7 @@ - if @user.solo_owned_groups.present? %p Your account is currently an owner in these groups: - %strong #{@user.solo_owned_groups.map(&:name).join(', ')} + %strong= @user.solo_owned_groups.map(&:name).join(', ') %p You must transfer ownership or delete these groups before you can delete your account. .append-bottom-default diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index 3276db6692c..d2a60ac2867 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -6,6 +6,9 @@ = key.title .description = key.fingerprint + .last-used-at + last used: + = key.last_used_at ? time_ago_with_tooltip(key.last_used_at) : 'n/a' .pull-right %span.key-created-at created #{time_ago_with_tooltip(key.created_at)} diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index dd7615400dc..d44603c638c 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -11,6 +11,9 @@ %li %span.light Created on: %strong= @key.created_at.to_s(:medium) + %li + %span.light Last used on: + %strong= @key.last_used_at.try(:to_s, :medium) || 'N/A' .col-md-8 %p diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index bb4effeeeb1..60a561c9f9c 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -19,7 +19,7 @@ Your New Personal Access Token .form-group = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control", 'aria-describedby' => "created-personal-access-token-help-block" - = clipboard_button(clipboard_text: flash[:personal_access_token]) + = clipboard_button(clipboard_text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left") %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. %hr diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 2385a90401e..d551754a2e5 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -18,7 +18,8 @@ or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host} .col-lg-9 .clearfix.avatar-image.append-bottom-default - = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' + = link_to avatar_icon(@user, 400), target: '_blank' do + = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160' %h5.prepend-top-0 Upload new avatar .prepend-top-5.append-bottom-10 @@ -61,7 +62,7 @@ %span.help-block Please click the link in the confirmation email before continuing. It was sent to = succeed "." do - %strong #{@user.unconfirmed_email} + %strong= @user.unconfirmed_email %p = link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 085f79de785..23e27c1105c 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -23,7 +23,7 @@ = markdown_toolbar_button({ icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) = markdown_toolbar_button({ icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) .toolbar-group - %button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } + %button.toolbar-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } } = icon("arrows-alt fw") .md-write-holder diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index afe2fd7fd7b..1a1327fb53c 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -8,7 +8,7 @@ %br %span.descr Builds need to be configured to enable this feature. - = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds') + = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds') .checkbox = form.label :only_allow_merge_if_all_discussions_are_resolved do = form.check_box :only_allow_merge_if_all_discussions_are_resolved diff --git a/app/views/projects/_visibility_select.html.haml b/app/views/projects/_visibility_select.html.haml new file mode 100644 index 00000000000..65fc0a36ca9 --- /dev/null +++ b/app/views/projects/_visibility_select.html.haml @@ -0,0 +1,7 @@ +- if can_change_visibility_level?(@project, current_user) + = form.select(model_method, visibility_select_options(@project, selected_level), {}, class: 'form-control visibility-select') +- else + .info.js-locked{ data: { help_block: visibility_level_description(@project.visibility_level, @project) } } + = visibility_level_icon(@project.visibility_level) + %strong + = visibility_level_label(@project.visibility_level) diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 350bdf5f836..f75f438ee4f 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -18,7 +18,7 @@ - else = link_to title, '#' -%ul.blob-commit-info.hidden-xs +%ul.blob-commit-info.table-list.hidden-xs - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) = render blob_commit, project: @project, ref: @ref diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 1d058daa094..228ac61fc8c 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -34,7 +34,7 @@ = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' .file-editor.code - %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]} + %pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data] - if local_assigns[:path] .js-edit-mode-pane#preview.hide .center diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml index a486b2fe491..f864702d862 100644 --- a/app/views/projects/blob/_image.html.haml +++ b/app/views/projects/blob/_image.html.haml @@ -1,8 +1,8 @@ .file-content.image_file - if blob.svg? - if blob.size_within_svg_limits? - - # We need to scrub SVG but we cannot do so in the RawController: it would - - # be wrong/strange if RawController modified the data. + -# We need to scrub SVG but we cannot do so in the RawController: it would + -# be wrong/strange if RawController modified the data. - blob.load_all_data!(@repository) - blob = sanitize_svg(blob) %img{ src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}", alt: "#{blob.name}" } diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 61a7ffdd0ab..4924c73cf8e 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -3,7 +3,7 @@ .modal-content .modal-header %a.close{ href: "#", "data-dismiss" => "modal" } × - %h3.page-title #{title} + %h3.page-title= title .modal-body = form_tag form_path, method: method, class: 'js-quick-submit js-upload-blob-form form-horizontal' do .dropzone diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml index 2125c3387c4..df7fa9ddaf2 100644 --- a/app/views/projects/boards/components/_sidebar.html.haml +++ b/app/views/projects/boards/components/_sidebar.html.haml @@ -1,23 +1,24 @@ %board-sidebar{ "inline-template" => true, ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" } - %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" } - .issuable-sidebar - .block.issuable-sidebar-header - %span.issuable-header-text.hide-collapsed.pull-left - %strong - {{ issue.title }} - %br/ - %span - = precede "#" do - {{ issue.id }} - %a.gutter-toggle.pull-right{ role: "button", - href: "#", - "@click.prevent" => "closeSidebar", - "aria-label" => "Toggle sidebar" } - = custom_icon("icon_close", size: 15) - .js-issuable-update - = render "projects/boards/components/sidebar/assignee" - = render "projects/boards/components/sidebar/milestone" - = render "projects/boards/components/sidebar/due_date" - = render "projects/boards/components/sidebar/labels" - = render "projects/boards/components/sidebar/notifications" + %transition{ name: "boards-sidebar-slide" } + %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" } + .issuable-sidebar + .block.issuable-sidebar-header + %span.issuable-header-text.hide-collapsed.pull-left + %strong + {{ issue.title }} + %br/ + %span + = precede "#" do + {{ issue.id }} + %a.gutter-toggle.pull-right{ role: "button", + href: "#", + "@click.prevent" => "closeSidebar", + "aria-label" => "Toggle sidebar" } + = custom_icon("icon_close", size: 15) + .js-issuable-update + = render "projects/boards/components/sidebar/assignee" + = render "projects/boards/components/sidebar/milestone" + = render "projects/boards/components/sidebar/due_date" + = render "projects/boards/components/sidebar/labels" + = render "projects/boards/components/sidebar/notifications" diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 2eb49685f08..19ffe73a08d 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -17,9 +17,8 @@ - if @project.protected_branch? branch.name %span.label.label-success - %i.fa.fa-lock protected - .controls.hidden-xs + .controls.hidden-xs< - if merge_project && create_mr_button?(@repository.root_ref, branch.name) = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do Merge Request diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index ecd812312c0..bd1f2d96f56 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -5,13 +5,14 @@ %div{ class: container_class } .top-area.adjust .nav-text - Protected branches can be managed in project settings + Protected branches can be managed in + = link_to 'project settings', namespace_project_protected_branches_path(@project.namespace, @project) .nav-controls = form_tag(filter_branches_path, method: :get) do = search_field_tag :search, params[:search], { placeholder: 'Filter by branch name', id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false } - .dropdown.inline + .dropdown.inline> %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } %span.light = projects_sort_options_hash[@sort] diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index b15be0d861d..736b485bf06 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,8 +1,8 @@ .content-block.build-header .header-content - = render 'ci/status/badge', status: @build.detailed_status(current_user) + = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false Build - %strong ##{@build.id} + %strong.js-build-id ##{@build.id} in pipeline = link_to pipeline_path(@build.pipeline) do %strong ##{@build.pipeline.id} diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 0b3adcbe121..37bf085130a 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -22,14 +22,14 @@ %p.build-detail-row The artifacts were removed #{time_ago_with_tooltip(@build.artifacts_expire_at)} - - elsif @build.artifacts_expire_at + - elsif @build.has_expiring_artifacts? %p.build-detail-row The artifacts will be removed in %span.js-artifacts-remove= @build.artifacts_expire_at - if @build.artifacts? .btn-group.btn-group-justified{ role: :group } - - if @build.artifacts_expire_at + - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build) = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do Keep diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 54724ef5cab..c613e473e4c 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -51,8 +51,10 @@ .prepend-top-default - if @build.erased? .erased.alert.alert-warning - - erased_by = "by #{link_to @build.erased_by.name, user_path(@build.erased_by)}" if @build.erased_by - Build has been erased #{erased_by.html_safe} #{time_ago_with_tooltip(@build.erased_at)} + - if @build.erased_by_user? + Build has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} + - else + Build has been erased #{time_ago_with_tooltip(@build.erased_at)} - else #js-build-scroll.scroll-controls .scroll-step diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 324a7f8cd3f..762ff34a9ec 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,5 +1,5 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - .project-action-button.dropdown.inline + .project-action-button.dropdown.inline> %button.btn{ 'data-toggle' => 'dropdown' } = icon('download') = icon("caret-down") diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 520113639b7..c1e496455d1 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -85,7 +85,7 @@ - if build.finished_at %p.finished-at = icon("calendar") - %span #{time_ago_with_tooltip(build.finished_at)} + %span= time_ago_with_tooltip(build.finished_at) %td.coverage - if coverage && build.try(:coverage) diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index e67492a36d1..818a70f38f1 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -47,21 +47,18 @@ - icon_status = "#{detailed_status.icon}_borderless" - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" - .stage-container.mini-pipeline-graph - .dropdown.inline.build-content - %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } } - %span.has-tooltip{ class: status_klass } - %span.mini-pipeline-graph-icon-container - %span{ class: status_klass }= custom_icon(icon_status) - = icon('caret-down', class: 'dropdown-caret') + .stage-container.dropdown.js-mini-pipeline-graph + %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } } + = custom_icon(icon_status) + = icon('caret-down') - .js-builds-dropdown-container - .dropdown-menu.grouped-pipeline-dropdown - .arrow-up - .js-builds-dropdown-list + %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container + .arrow-up + .js-builds-dropdown-list.scrollable-menu + + .js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin - .js-builds-dropdown-loading.builds-dropdown-loading.hidden - %span.fa.fa-spinner.fa-spin %td - if pipeline.duration @@ -81,18 +78,18 @@ .btn-group.inline - if actions.any? .btn-group - %a.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' } + %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual build' } = custom_icon('icon_play') - = icon('caret-down') + = icon('caret-down', 'aria-hidden' => 'true') %ul.dropdown-menu.dropdown-menu-align-right - actions.each do |build| %li = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do = custom_icon('icon_play') - %span= build.name.humanize + %span= build.name - if artifacts.present? .btn-group - %a.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' } + %button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Artifacts' } = icon("download") = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right @@ -105,8 +102,8 @@ - if can?(current_user, :update_pipeline, pipeline.project) .cancel-retry-btns.inline - if pipeline.retryable? - = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do + = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Retry' , method: :post do = icon("repeat") - if pipeline.cancelable? - = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do + = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do = icon("remove") diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 12e4280d344..421b3db342d 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -13,7 +13,7 @@ %a.close{ href: "#", "data-dismiss" => "modal" } × %h3.page-title== #{label} this #{commit.change_type_title(current_user)} .modal-body - = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do + = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "form-horizontal js-#{type}-form js-requires-input" do .form-group.branch = label_tag 'target_branch', target_label, class: 'control-label' .col-sm-10 diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index 13ab2253733..8aed88da38b 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -7,4 +7,4 @@ = nav_link(path: 'commit#pipelines') do = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Pipelines - %span.badge= @ci_pipelines.count + %span.badge= @commit.pipelines.size diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index a9ee9230076..6dba42d5226 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,8 +1,7 @@ .page-content-header .header-main-content - %strong - = clipboard_button(clipboard_text: @commit.id) - = @commit.short_id + %strong Commit #{@commit.short_id} + = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard") %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} %span by diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 5a9f7295135..1164627fa11 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -1,9 +1,9 @@ -%ul.content-list.pipelines +%div - if pipelines.blank? - %li + %div .nothing-here-block No pipelines to show - else - .table-holder + .table-holder.pipelines %table.table.ci-table.js-pipeline-table %thead %th.pipeline-status Status diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index 8233e26e4e7..00e7cdd1729 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -3,4 +3,4 @@ = render "commit_box" = render "ci_menu" -= render "pipelines_list", pipelines: @ci_pipelines += render "pipelines_list", pipelines: @commit.pipelines.order(id: :desc) diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index a940515fadf..002e3d345dc 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -9,33 +9,33 @@ - cache_key.push(commit.status(ref)) if commit.status(ref) = cache(cache_key, expires_in: 1.day) do - %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" } - = author_avatar(commit, size: 36) + %li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" } - .commit-info-block - .commit-row-title - %span.item-title - = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message" - %span.commit-row-message.visible-xs-inline - · - = commit.short_id - - if commit.status(ref) - .visible-xs-inline - = render_commit_status(commit, ref: ref) - - if commit.description? - %a.text-expander.hidden-xs.js-toggle-button ... + .table-list-cell.avatar-cell.hidden-xs + = author_avatar(commit, size: 36) - .commit-actions.hidden-xs - - if commit.status(ref) - = render_commit_status(commit, ref: ref) - = clipboard_button(clipboard_text: commit.id) - = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" - = link_to_browse_code(project, commit) + .table-list-cell.commit-content + = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title" + %span.commit-row-message.visible-xs-inline + · + = commit.short_id + - if commit.status(ref) + .visible-xs-inline + = render_commit_status(commit, ref: ref) + - if commit.description? + %a.text-expander.hidden-xs.js-toggle-button ... - if commit.description? %pre.commit-row-description.js-toggle-content = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author)) + .commiter + = commit_author_link(commit, avatar: false, size: 24) + committed + #{time_ago_with_tooltip(commit.committed_date)} - = commit_author_link(commit, avatar: false, size: 24) - committed - #{time_ago_with_tooltip(commit.committed_date)} + .table-list-cell.commit-actions.hidden-xs + - if commit.status(ref) + = render_commit_status(commit, ref: ref) + = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard") + = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent" + = link_to_browse_code(project, commit) diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml index 6f5835cb9be..64d93e4141c 100644 --- a/app/views/projects/commits/_commit_list.html.haml +++ b/app/views/projects/commits/_commit_list.html.haml @@ -11,4 +11,4 @@ %li.warning-row.unstyled #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues. - else - %ul.content-list= render commits, project: @project, ref: @ref + %ul.content-list.table-list= render commits, project: @project, ref: @ref diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index 48756c68941..904cdb5767f 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -2,9 +2,9 @@ - commits, hidden = limited_commits(@commits) - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| - %li.commit-header= "#{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}" + %li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')} %li.commits-row - %ul.list-unstyled.commit-list + %ul.content-list.commit-list.table-list.table-wide = render commits, project: project, ref: ref - if hidden > 0 diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index e77f23c7fd8..d94f23f5a38 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -9,10 +9,13 @@ = render "head" %div{ class: container_class } - .row-content-block.second-block.content-component-block + .row-content-block.second-block.content-component-block.flex-container-block .tree-ref-holder = render 'shared/ref_switcher', destination: 'commits' + %ul.breadcrumb.repo-breadcrumb + = commits_breadcrumbs + .block-controls.hidden-xs.hidden-sm - if @merge_request.present? .control @@ -30,8 +33,6 @@ .control = link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do = icon("rss") - %ul.breadcrumb.repo-breadcrumb - = commits_breadcrumbs %div{ id: dom_id(@project) } %ol#commits-list.list-unstyled.content_list diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 7bde20c3286..d76d48187cd 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -2,21 +2,21 @@ .clearfix - if params[:to] && params[:from] .compare-switch-container - = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'} + = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Switch base of comparison'} .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown .input-group.inline-input-group %span.input-group-addon from = hidden_field_tag :from, params[:from] - = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do - .dropdown-toggle-text= params[:from] || 'Select branch/tag' + = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do + .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag' = render "ref_dropdown" .compare-ellipsis.inline ... .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group %span.input-group-addon to = hidden_field_tag :to, params[:to] - = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do - .dropdown-toggle-text= params[:to] || 'Select branch/tag' + = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do + .dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag' = render "ref_dropdown" = button_tag "Compare", class: "btn btn-create commits-compare-btn" diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 819e9bc15ae..9c8f58d4aea 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -16,9 +16,9 @@ There isn't anything to compare. %p.slead - if params[:to] == params[:from] - %span.label-branch #{params[:from]} + %span.label-branch= params[:from] and - %span.label-branch #{params[:to]} + %span.label-branch= params[:to] are the same. - else You'll need to use different branch names to get a valid comparison. diff --git a/app/views/projects/cycle_analytics/_empty_stage.html.haml b/app/views/projects/cycle_analytics/_empty_stage.html.haml index b200ce22970..c3f95860e92 100644 --- a/app/views/projects/cycle_analytics/_empty_stage.html.haml +++ b/app/views/projects/cycle_analytics/_empty_stage.html.haml @@ -2,6 +2,6 @@ .empty-stage .icon-no-data = custom_icon ('icon_no_data') - %h4 We don’t have enough data to show this stage. + %h4 We don't have enough data to show this stage. %p {{currentStage.emptyStageText}} diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 89e2e162b5b..479ce44f378 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -28,8 +28,8 @@ .container-fluid .row .col-sm-3.col-xs-12.column{ "v-for" => "item in state.summary" } - %h3.header {{item.value}} - %p.text {{item.title}} + %h3.header {{ item.value }} + %p.text {{ item.title }} .col-sm-3.col-xs-12.column .dropdown.inline.js-ca-dropdown %button.dropdown-menu-toggle{ "data-toggle" => "dropdown", :type => "button" } diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index 9238f232c7e..c468202569f 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -1,6 +1,6 @@ %tr.deployment %td - %strong= "##{deployment.iid}" + %strong ##{deployment.iid} %td = render 'projects/deployments/commit', deployment: deployment @@ -8,7 +8,7 @@ %td.build-column - if deployment.deployable = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do - = "#{deployment.deployable.name} (##{deployment.deployable.id})" + #{deployment.deployable.name} (##{deployment.deployable.id}) - if deployment.user by = user_avatar(user: deployment.user, size: 20) diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index 52a1ece7d60..b87b79b170e 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -1,5 +1,5 @@ .diff-content.diff-wrap-lines - - # Skip all non non-supported blobs + -# Skip all non non-supported blobs - return unless blob.respond_to?(:text?) - if diff_file.too_large? .nothing-here-block This diff could not be displayed because it is too large. diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index ab4a2dc36e5..58c20e225c6 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -18,8 +18,8 @@ = parallel_diff_btn = render 'projects/diffs/stats', diff_files: diff_files -- if diff_files.overflow? - = render 'projects/diffs/warning', diff_files: diff_files +- if render_overflow_warning?(diff_files) + = render 'projects/diffs/warning', diff_files: diffs .files{ data: { can_create_note: can_create_note } } - diff_files.each_with_index do |diff_file| diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 15df2edefc7..c37a33bbcd5 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,5 +1,5 @@ .diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) } - .file-title{ id: "file-path-#{hexdigest(diff_file.file_path)}" } + .file-title = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}" - unless diff_file.submodule? diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml index 90c9a0c6c2b..ddec775b789 100644 --- a/app/views/projects/diffs/_file_header.html.haml +++ b/app/views/projects/diffs/_file_header.html.haml @@ -25,4 +25,4 @@ - if diff_file.mode_changed? %small - = "#{diff_file.a_mode} → #{diff_file.b_mode}" + #{diff_file.a_mode} → #{diff_file.b_mode} diff --git a/app/views/projects/diffs/_image.html.haml b/app/views/projects/diffs/_image.html.haml index 1bccaaf5273..ca10921c5e2 100644 --- a/app/views/projects/diffs/_image.html.haml +++ b/app/views/projects/diffs/_image.html.haml @@ -9,7 +9,7 @@ %span.wrap .frame{ class: image_diff_class(diff) } %img{ src: diff.deleted_file ? old_file_raw_path : file_raw_path, alt: diff.new_path } - %p.image-info= "#{number_to_human_size file.size}" + %p.image-info= number_to_human_size(file.size) - else .image .two-up.view @@ -18,7 +18,7 @@ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.old_ref, diff.old_path)) } %img{ src: old_file_raw_path, alt: diff.old_path } %p.image-info.hide - %span.meta-filesize= "#{number_to_human_size old_file.size}" + %span.meta-filesize= number_to_human_size(old_file.size) | %b W: %span.meta-width @@ -30,7 +30,7 @@ %a{ href: namespace_project_blob_path(@project.namespace, @project, tree_join(diff_file.new_ref, diff.new_path)) } %img{ src: file_raw_path, alt: diff.new_path } %p.image-info.hide - %span.meta-filesize= "#{number_to_human_size file.size}" + %span.meta-filesize= number_to_human_size(file.size) | %b W: %span.meta-width diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index b087485aa17..f361204ecac 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -14,7 +14,7 @@ - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } } - %a{ href: "##{left_line_code}" }= raw(left.old_pos) + %a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } } %td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text) - else %td.old_line.diff-line-num.empty-cell @@ -27,7 +27,7 @@ - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } } - %a{ href: "##{right_line_code}" }= raw(right.new_pos) + %a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } } %td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text) - else %td.old_line.diff-line-num.empty-cell diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index 290f696d582..8e24e28765f 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -2,7 +2,7 @@ .commit-stat-summary Showing = link_to '#', class: 'js-toggle-button' do - %strong #{pluralize(diff_files.size, "changed file")} + %strong= pluralize(diff_files.size, "changed file") with %strong.cgreen #{diff_files.sum(&:added_lines)} additions and diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 38e7fc4279c..84787155168 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -7,10 +7,17 @@ .project-edit-errors = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f| %fieldset.append-bottom-0 - .form-group - = f.label :name, class: 'label-light' do - Project name - = f.text_field :name, class: "form-control", id: "project_name_edit" + .row + .form-group.col-md-9 + = f.label :name, class: 'label-light', for: 'project_name_edit' do + Project name + = f.text_field :name, class: "form-control", id: "project_name_edit" + + .form-group.col-md-3 + = f.label :id, class: 'label-light' do + Project ID + = f.text_field :id, class: 'form-control', readonly: true + .form-group = f.label :description, class: 'label-light' do Project description @@ -21,76 +28,68 @@ .form-group = f.label :default_branch, "Default Branch", class: 'label-light' = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) - .form-group.project-visibility-level-holder - = f.label :visibility_level, class: 'label-light' do - Visibility Level - = link_to "(?)", help_page_path("public_access/public_access") - - if can_change_visibility_level?(@project, current_user) - = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project) - - else - .info - = visibility_level_icon(@project.visibility_level) - %strong - = visibility_level_label(@project.visibility_level) - .light= visibility_level_description(@project.visibility_level, @project) - - .form-group - = render 'shared/allow_request_access', form: f - .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" %p.help-block Separate tags with commas. %hr - %fieldset.features.append-bottom-0 + %fieldset.append-bottom-0 %h5.prepend-top-0 - Feature Visibility - - = f.fields_for :project_feature do |feature_fields| - .form_group.prepend-top-20 - .row - .col-md-9 - = feature_fields.label :repository_access_level, "Repository", class: 'label-light' - %span.help-block Push files to be stored in this project - .col-md-3.js-repo-access-level - = project_feature_access_select(:repository_access_level) - - .col-sm-12 - .row - .col-md-9.project-feature-nested - = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' - %span.help-block Submit changes to be merged upstream - .col-md-3 - = project_feature_access_select(:merge_requests_access_level) + Sharing & Permissions + .form_group.prepend-top-20.sharing-and-permissions + .row.js-visibility-select + .col-md-9 + %label.label-light + = label_tag :project_visibility, 'Project Visibility', class: 'label-light' + = link_to "(?)", help_page_path("public_access/public_access") + %span.help-block + .col-md-3.visibility-select-container + = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level) + = f.fields_for :project_feature do |feature_fields| + %fieldset.features + .row + .col-md-9.project-feature + = feature_fields.label :repository_access_level, "Repository", class: 'label-light' + %span.help-block View and edit files in this project + .col-md-3.js-repo-access-level + = project_feature_access_select(:repository_access_level) - .row - .col-md-9.project-feature-nested - = feature_fields.label :builds_access_level, "Builds", class: 'label-light' - %span.help-block Submit, test and deploy your changes before merge - .col-md-3 - = project_feature_access_select(:builds_access_level) + .row + .col-md-9.project-feature.nested + = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light' + %span.help-block Submit changes to be merged upstream + .col-md-3 + = project_feature_access_select(:merge_requests_access_level) - .row - .col-md-9 - = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' - %span.help-block Share code pastes with others out of Git repository - .col-md-3 - = project_feature_access_select(:snippets_access_level) + .row + .col-md-9.project-feature.nested + = feature_fields.label :builds_access_level, "Builds", class: 'label-light' + %span.help-block Submit, test and deploy your changes before merge + .col-md-3 + = project_feature_access_select(:builds_access_level) - .row - .col-md-9 - = feature_fields.label :issues_access_level, "Issues", class: 'label-light' - %span.help-block Lightweight issue tracking system for this project - .col-md-3 - = project_feature_access_select(:issues_access_level) + .row + .col-md-9.project-feature + = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light' + %span.help-block Share code pastes with others out of Git repository + .col-md-3 + = project_feature_access_select(:snippets_access_level) - .row - .col-md-9 - = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' - %span.help-block Pages for project documentation - .col-md-3 - = project_feature_access_select(:wiki_access_level) + .row + .col-md-9.project-feature + = feature_fields.label :issues_access_level, "Issues", class: 'label-light' + %span.help-block Lightweight issue tracking system for this project + .col-md-3 + = project_feature_access_select(:issues_access_level) + .row + .col-md-9.project-feature + = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light' + %span.help-block Pages for project documentation + .col-md-3 + = project_feature_access_select(:wiki_access_level) + .form-group + = render 'shared/allow_request_access', form: f - if Gitlab.config.lfs.enabled && current_user.admin? .row .col-md-9 diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 3525a07a687..58c085cdb9d 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -52,7 +52,7 @@ git push -u origin master %fieldset - %h5 Existing folder or Git repository + %h5 Existing folder %pre.light-well :preserve cd existing_folder @@ -62,6 +62,15 @@ git commit git push -u origin master + %fieldset + %h5 Existing Git repository + %pre.light-well + :preserve + cd existing_repo + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git push -u origin --all + git push -u origin --tags + - if can? current_user, :remove_project, @project .prepend-top-20 = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right" diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 6c8a6f051a9..f4aa523b32d 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -1,7 +1,7 @@ .top-area .nav-text - full_count_title = "#{@public_forks_count} public and #{@private_forks_count} private" - = "#{pluralize(@total_forks_count, 'fork')}: #{full_count_title}" + #{pluralize(@total_forks_count, 'fork')}: #{full_count_title} .nav-controls = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f| diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml index 6d7af1685fd..07fb80750d6 100644 --- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml +++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml @@ -77,7 +77,7 @@ - if generic_commit_status.finished_at %p.finished-at = icon("calendar") - %span #{time_ago_with_tooltip(generic_commit_status.finished_at)} + %span= time_ago_with_tooltip(generic_commit_status.finished_at) %td.coverage - if coverage && generic_commit_status.try(:coverage) diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml index 7e34a89f9ae..c8a82f7bca3 100644 --- a/app/views/projects/graphs/commits.html.haml +++ b/app/views/projects/graphs/commits.html.haml @@ -11,7 +11,7 @@ %p.lead Commit statistics for - %strong #{@ref} + %strong= @ref #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')} .row @@ -19,19 +19,19 @@ %ul %li %p.lead - %strong #{@commits_graph.commits.size} + %strong= @commits_graph.commits.size commits during - %strong #{@commits_graph.duration} + %strong= @commits_graph.duration days %li %p.lead Average - %strong #{@commits_graph.commit_per_day} + %strong= @commits_graph.commit_per_day commits per day %li %p.lead Contributed by - %strong #{@commits_graph.authors} + %strong= @commits_graph.authors authors .col-md-6 %div diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/_index.html.haml index 1b0dbbb8111..99d0df2ac34 100644 --- a/app/views/projects/group_links/index.html.haml +++ b/app/views/projects/group_links/_index.html.haml @@ -20,10 +20,10 @@ .form-group = label_tag :expires_at, 'Access expiration date', class: 'label-light' .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Select access expiration date', id: 'expires_at_groups' %i.clear-icon.js-clear-input .help-block - On this date, all users in the group will automatically lose access to this project. + On this date, all members in the group will automatically lose access to this project. = submit_tag "Share", class: "btn btn-create" .col-lg-9.col-lg-offset-3 %hr diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/_index.html.haml index 8faad351463..8faad351463 100644 --- a/app/views/projects/hooks/index.html.haml +++ b/app/views/projects/hooks/_index.html.haml diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index 26f3f0ac292..5fbed8b9ab8 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -6,6 +6,9 @@ = content_for :sub_nav do = render "projects/issues/head" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('filtered_search/filtered_search_bundle.js') + = content_for :meta_tags do - if current_user = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues") @@ -16,11 +19,8 @@ = render 'shared/issuable/nav', type: :issues .nav-controls - if current_user - = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn append-right-10' do + = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do = icon('rss') - %span.icon-label - Subscribe - = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project) - if can? current_user, :create_issue, @project = link_to new_namespace_project_issue_path(@project.namespace, @project, @@ -30,7 +30,7 @@ title: "New Issue", id: "new_issue_link" do New Issue - = render 'shared/issuable/filter', type: :issues + = render 'shared/issuable/search_bar', type: :issues .issues-holder = render 'issues' diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 981bf640a6b..9fa00811af0 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -1,7 +1,9 @@ -- @content_class = "limit-container-width" +- @content_class = "limit-container-width" unless fluid_layout - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') .clearfix.detail-page-header .issuable-header diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index 24e86b8497f..a80f9aa4c4a 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -7,20 +7,21 @@ %p = @teams.one? ? 'The team' : 'Select the team' where the slash commands will be used in - - selected_id = @teams.keys.first if @teams.one? + - selected_id = @teams.one? ? @teams.keys.first : 0 - options = mattermost_teams_options(@teams) - options = options_for_select(options, selected_id) - = f.select(:team_id, options, {}, { class: 'form-control', selected: "#{selected_id}" }) + = f.select(:team_id, options, {}, { class: 'form-control', disabled: @teams.one?, selected: selected_id }) + = f.hidden_field(:team_id, value: selected_id) if @teams.one? .help-block - if @teams.one? - This is the only team where you are an administrator. + This is the only available team. - else - The list shows teams where you are administrator - To create a team, ask your Mattermost system administrator. + The list shows all available teams. To create a team, = link_to "#{Gitlab.config.mattermost.host}/create_team" do use Mattermost's interface = icon('external-link') + or ask your Mattermost system administrator. %hr %h4 Command trigger word %p Choose the word that will trigger commands diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 34ead6427e0..d3c013b3f21 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -3,15 +3,15 @@ %p.slead - source_title, target_title = format_mr_branch_names(@merge_request) From - %strong.label-branch #{source_title} + %strong.label-branch= source_title %span into - %strong.label-branch #{target_title} + %strong.label-branch= target_title %span.pull-right = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal common-note-form js-requires-input js-quick-submit' } do |f| - = render 'shared/issuable/form', f: f, issuable: @merge_request + = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits = f.hidden_field :source_project_id = f.hidden_field :source_branch = f.hidden_field :target_project_id @@ -43,7 +43,7 @@ #commits.commits.tab-pane.active = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX - if @pipelines.any? #pipelines.pipelines.tab-pane = render "projects/merge_requests/show/pipelines" diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 633701c6f40..9585a9a3ad4 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -1,8 +1,9 @@ -- @content_class = "limit-container-width" +- @content_class = "limit-container-width" unless fluid_layout - page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests" - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') .merge-request{ 'data-url' => merge_request_path(@merge_request) } @@ -42,64 +43,63 @@ = render "projects/merge_requests/widget/show.html.haml" - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user) - .merge-manually.light.prepend-top-default.append-bottom-default + .merge-manually.light.prepend-top-default You can also accept this merge request manually using the = succeed '.' do = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" - .content-block.content-block-small + .content-block.content-block-small.emoji-list-container = render 'award_emoji/awards_block', awardable: @merge_request, inline: true - - if @commits_count.nonzero? - .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } - %div{ class: container_class } - %ul.merge-request-tabs.nav-links.no-top.no-bottom - %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do - Discussion - %span.badge= @merge_request.related_notes.user.count - - if @merge_request.source_project - %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do - Commits - %span.badge= @commits_count - - if @pipelines.any? - %li.pipelines-tab - = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do - Pipelines - %span.badge= @pipelines.size - %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do - Changes - %span.badge= @merge_request.diff_size - %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } - %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } - %div - .line-resolve-all{ "v-show" => "discussionCount > 0", - ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } - %span.line-resolve-btn.is-disabled{ type: "button", - ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } - = render "shared/icons/icon_status_success.svg" - %span.line-resolve-text - {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved - = render "discussions/jump_to_next" + .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } + .merge-request-tabs-container + %ul.merge-request-tabs.nav-links.no-top.no-bottom + %li.notes-tab + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do + Discussion + %span.badge= @merge_request.related_notes.user.count + - if @merge_request.source_project + %li.commits-tab + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do + Commits + %span.badge= @commits_count + - if @pipelines.any? + %li.pipelines-tab + = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do + Pipelines + %span.badge= @pipelines.size + %li.diffs-tab + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do + Changes + %span.badge= @merge_request.diff_size + %li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true } + %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" } + %div + .line-resolve-all{ "v-show" => "discussionCount > 0", + ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" } + %span.line-resolve-btn.is-disabled{ type: "button", + ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" } + = render "shared/icons/icon_status_success.svg" + %span.line-resolve-text + {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved + = render "discussions/jump_to_next" - .tab-content#diff-notes-app - #notes.notes.tab-pane.voting_notes - .row - %section.col-md-12 - .issuable-discussion - = render "projects/merge_requests/discussion" + .tab-content#diff-notes-app + #notes.notes.tab-pane.voting_notes + .row + %section.col-md-12 + .issuable-discussion + = render "projects/merge_requests/discussion" - #commits.commits.tab-pane - - # This tab is always loaded via AJAX - #pipelines.pipelines.tab-pane - - # This tab is always loaded via AJAX - #diffs.diffs.tab-pane - - # This tab is always loaded via AJAX + #commits.commits.tab-pane + -# This tab is always loaded via AJAX + #pipelines.pipelines.tab-pane + -# This tab is always loaded via AJAX + #diffs.diffs.tab-pane + -# This tab is always loaded via AJAX - .mr-loading-status - = spinner + .mr-loading-status + = spinner = render 'shared/issuable/sidebar', issuable: @merge_request - if @merge_request.can_be_reverted?(current_user) @@ -113,3 +113,5 @@ merge_request = new MergeRequest({ action: "#{controller.action_name}" }); + + var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}"; diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index b8b87dcdcaf..ebef2157d34 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -1,5 +1,6 @@ - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/vue_resource.js') = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js') = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/show/mr_title" diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml index baa1ade5eee..11793919ff7 100644 --- a/app/views/projects/merge_requests/show/_commits.html.haml +++ b/app/views/projects/merge_requests/show/_commits.html.haml @@ -1,2 +1,8 @@ -%ol#commits-list.list-unstyled - = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch +- if @commits.empty? + .commits-empty + %h4 + There are no commits yet. + = custom_icon ('illustration_no_commits') +- else + %ol#commits-list.list-unstyled + = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 99c71e1454a..5f048d04b27 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,13 +1,5 @@ -- if @merge_request_diff.collected? +- if @merge_request_diff.collected? || @merge_request_diff.overflow? = render 'projects/merge_requests/show/versions' = render "projects/diffs/diffs", diffs: @diffs - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} -- else - .alert.alert-warning - %h4 - Changes view for this comparison is extremely large. - %p - You can - = link_to "download it", merge_request_path(@merge_request, format: :diff), class: "vlink" - instead. 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 ec76c6a5417..93ed4b68e0e 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 @@ -8,7 +8,7 @@ %p %strong Step 1. Fetch and check out the branch for this merge request - = clipboard_button(clipboard_target: "pre#merge-info-1") + = clipboard_button(clipboard_target: "pre#merge-info-1", title: "Copy commands to clipboard") %pre.dark#merge-info-1 - if @merge_request.for_fork? :preserve @@ -25,7 +25,7 @@ %p %strong Step 3. Merge the branch and fix any conflicts that come up - = clipboard_button(clipboard_target: "pre#merge-info-3") + = clipboard_button(clipboard_target: "pre#merge-info-3", title: "Copy commands to clipboard") %pre.dark#merge-info-3 - if @merge_request.for_fork? :preserve @@ -38,7 +38,7 @@ %p %strong Step 4. Push the result of the merge to GitLab - = clipboard_button(clipboard_target: "pre#merge-info-4") + = clipboard_button(clipboard_target: "pre#merge-info-4", title: "Copy commands to clipboard") %pre.dark#merge-info-4 :preserve git push origin #{h @merge_request.target_branch} diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index ac4a03220b9..74a7b1dc498 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -19,13 +19,13 @@ %ul - @merge_request_diffs.each do |merge_request_diff| %li - = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do + = link_to merge_request_version_path(@project, @merge_request, merge_request_diff, @start_sha), class: ('is-active' if merge_request_diff == @merge_request_diff) do %strong - if merge_request_diff.latest? latest version - else version #{version_index(merge_request_diff)} - .monospace #{short_sha(merge_request_diff.head_commit_sha)} + .monospace= short_sha(merge_request_diff.head_commit_sha) %small #{number_with_delimiter(merge_request_diff.commits_count)} #{'commit'.pluralize(merge_request_diff.commits_count)}, = time_ago_with_tooltip(merge_request_diff.created_at) @@ -55,14 +55,14 @@ latest version - else version #{version_index(merge_request_diff)} - .monospace #{short_sha(merge_request_diff.head_commit_sha)} + .monospace= short_sha(merge_request_diff.head_commit_sha) %small = time_ago_with_tooltip(merge_request_diff.created_at) %li = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do %strong #{@merge_request.target_branch} (base) - .monospace #{short_sha(@merge_request_diff.base_commit_sha)} + .monospace= short_sha(@merge_request_diff.base_commit_sha) - if different_base?(@start_version, @merge_request_diff) .content-block @@ -72,7 +72,7 @@ = link_to namespace_project_compare_path(@project.namespace, @project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do new commits from - %code #{@merge_request.target_branch} + %code= @merge_request.target_branch - unless @merge_request_diff.latest? && !@start_sha .comments-disabled-notif.content-block diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index c80dc33058d..5faa6c43f9f 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -13,8 +13,8 @@ %span.ci-coverage - elsif @merge_request.has_ci? - - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API + -# Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX + -# TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" } diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index 072d01d144e..f70cd09c5f4 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -1,3 +1,6 @@ +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + %h4 Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} to be merged automatically when the pipeline succeeds. diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 8620f492282..e8e450742b5 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -1,11 +1,14 @@ .note-edit-form - = form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true, html: { class: 'edit-note common-note-form js-quick-submit' } do |f| - = note_target_fields(note) - = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do - = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." + = form_tag '#', method: :put, class: 'edit-note common-note-form js-quick-submit' do + = hidden_field_tag :target_id, '', class: 'js-form-target-id' + = hidden_field_tag :target_type, '', class: 'js-form-target-type' + = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview', referenced_users: true } do + = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." = render 'projects/notes/hints' .note-form-actions.clearfix - = f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button' + .settings-message.note-edit-warning.js-edit-warning + Finish editing this message first! + = submit_tag 'Save Comment', class: 'btn btn-nr btn-save js-comment-button' %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } Cancel diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 39731668a61..b561052e721 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -3,6 +3,7 @@ = form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f| = hidden_field_tag :view, diff_view = hidden_field_tag :line_type + = hidden_field_tag :merge_request_diff_head_sha, @note.noteable.try(:diff_head_sha) = note_target_fields(@note) = f.hidden_field :commit_id = f.hidden_field :line_code diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml index eb869ea85bf..09339e520dd 100644 --- a/app/views/projects/notes/_note.html.haml +++ b/app/views/projects/notes/_note.html.haml @@ -67,7 +67,9 @@ = note.redacted_note_html = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - if note_editable - = render 'projects/notes/edit_form', note: note + .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } + #{note.note} + %textarea.hidden.js-task-list-field.original-task-list= note.note .note-awards = render 'award_emoji/awards_block', awardable: note, inline: false - if note.system diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 00b62a595ff..fbd2bff5bbb 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,5 +1,8 @@ %ul#notes-list.notes.main-notes-list.timeline = render "projects/notes/notes" + += render 'projects/notes/edit_form' + %ul.notes.notes-form.timeline %li.timeline-entry .flash-container.timeline-content @@ -20,4 +23,4 @@ to post a comment :javascript - var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") + var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, project_id: @project, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml index cf1b366bf2c..a0b14a7274a 100644 --- a/app/views/projects/pipelines/_stage.html.haml +++ b/app/views/projects/pipelines/_stage.html.haml @@ -1,4 +1,3 @@ -%ul - - @stage.statuses.latest.each do |status| - %li.dropdown-build - = render 'ci/status/graph_badge', subject: status +- @stage.statuses.latest.each do |status| + %li + = render 'ci/status/dropdown_graph_badge', subject: status diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 4bb3d4d35fb..df36279ed75 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -35,21 +35,33 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint - - .content-list.pipelines + .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } - if @pipelines.blank? %div .nothing-here-block No pipelines to show - else - .table-holder - %table.table.ci-table.js-pipeline-table - %thead - %th.pipeline-status Status - %th.pipeline-info Pipeline - %th.pipeline-commit Commit - %th.pipeline-stages Stages - %th.pipeline-date - %th.pipeline-actions.hidden-xs - = render @pipelines, commit_sha: true, stage: true, allow_retry: true - - = paginate @pipelines, theme: 'gitlab' + .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), + } } + + .vue-pipelines-index + += page_specific_javascript_tag('vue_pipelines_index/index.js') diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index 9738f369a35..c7996077bc7 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -1,7 +1,7 @@ .panel.panel-default .panel-heading Group members with access to - %strong #{@group.name} + %strong= @group.name %span.badge= members.size - if can?(current_user, :admin_group_member, @group) .controls diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml index d7f5fa96527..fdeb5f21fbe 100644 --- a/app/views/projects/project_members/_groups.html.haml +++ b/app/views/projects/project_members/_groups.html.haml @@ -1,7 +1,7 @@ .panel.panel-default.project-members-groups .panel-heading Groups with access to - %strong #{@project.name} + %strong= @project.name %span.badge= group_links.size %ul.content-list = render partial: 'shared/members/group', collection: group_links, as: :group_link diff --git a/app/views/projects/project_members/_index.html.haml b/app/views/projects/project_members/_index.html.haml new file mode 100644 index 00000000000..ab0771b5751 --- /dev/null +++ b/app/views/projects/project_members/_index.html.haml @@ -0,0 +1,22 @@ +.row.prepend-top-default + .col-lg-3.settings-sidebar + %h4.prepend-top-0 + Members + - if can?(current_user, :admin_project_member, @project) + %p + Add a new member to + %strong= @project.name + .col-lg-9 + .light.prepend-top-default + - if can?(current_user, :admin_project_member, @project) + = render "projects/project_members/new_project_member" + + = render 'shared/members/requests', membership_source: @project, requesters: @requesters + .append-bottom-default.clearfix + %h5.member.existing-title + Existing members and groups + - if @group_links.any? + = render 'projects/project_members/groups', group_links: @group_links + + = render 'projects/project_members/team', members: @project_members + = paginate @project_members, theme: "gitlab" diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 79dcd7a6ee9..2b1c23f7dda 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -1,22 +1,18 @@ = form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'users-project-form' } do |f| - .row - .col-md-4.col-lg-6 - = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true) - .help-block.append-bottom-10 - Search for users by name, username, or email, or invite new ones using their email address. - - .col-md-3.col-lg-2 - = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" - .help-block.append-bottom-10 - = link_to "Read more", help_page_path("user/permissions"), class: "vlink" - about role permissions - - .col-md-3.col-lg-2 - .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' - %i.clear-icon.js-clear-input - .help-block.append-bottom-10 - On this date, the user(s) will automatically lose access to this project. - - .col-md-2 - = f.submit "Add to project", class: "btn btn-create btn-block" + .form-group + = users_select_tag(:user_ids, multiple: true, class: "input-clamp", scope: :all, email_user: true, placeholder: "Search for members to update or invite") + .help-block.append-bottom-10 + Search for members by name, username, or email, or invite new ones using their email address. + .form-group + = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select" + .help-block.append-bottom-10 + = link_to "Read more", help_page_path("user/permissions"), class: "vlink" + about role permissions + .form-group + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' + %i.clear-icon.js-clear-input + .help-block.append-bottom-10 + On this date, the member(s) will automatically lose access to this project. + = f.submit "Add to project", class: "btn btn-create" + = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default", title: "Import members from another project" diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml index 77370c14def..7902ddb1ae9 100644 --- a/app/views/projects/project_members/_shared_group_members.html.haml +++ b/app/views/projects/project_members/_shared_group_members.html.haml @@ -5,9 +5,9 @@ .panel.panel-default .panel-heading Shared with - %strong #{shared_group.name} + %strong= shared_group.name group, members with - %strong #{group_links.human_access} + %strong= group_links.human_access role (#{shared_group_users_count}) - if can?(current_user, :admin_group, shared_group) .panel-head-actions diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index c1e894d8f40..81d57c77edf 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,7 +1,13 @@ .panel.panel-default .panel-heading - Users with access to - %strong #{@project.name} + Members with access to + %strong= @project.name %span.badge= @project_members.total_count + = form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do + .form-group + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") + = render 'shared/members/sort_dropdown' %ul.content-list = render partial: 'shared/members/member', collection: members, as: :member diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index eef97107d77..42ce4f8001b 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -12,5 +12,4 @@ .form-actions = button_tag 'Import project members', class: "btn btn-create" - = link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel" - + = link_to "Cancel", namespace_project_settings_members_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml deleted file mode 100644 index 4f1cec20f85..00000000000 --- a/app/views/projects/project_members/index.html.haml +++ /dev/null @@ -1,29 +0,0 @@ -- page_title "Members" - -.project-members-page.prepend-top-default - %h4.project-members-title.clearfix - Members - = link_to "Import", import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-default pull-right hidden-xs", title: "Import members from another project" - - if can?(current_user, :admin_project_member, @project) - .project-members-new.append-bottom-default - %p.clearfix - Add new user to - %strong= @project.name - = render "new_project_member" - - = render 'shared/members/requests', membership_source: @project, requesters: @requesters - - .append-bottom-default.clearfix - %h5.member.existing-title - Existing users and groups - = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do - .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } - = icon("search") - = render 'shared/members/sort_dropdown' - - if @group_links.any? - = render 'groups', group_links: @group_links - - = render 'team', members: @project_members - = paginate @project_members, theme: "gitlab" diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 33d5cbff420..79d8d721aa9 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -7,7 +7,7 @@ .oneline .title Release notes for tag - %strong #{@tag.name} + %strong= @tag.name = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f| diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 51b0939564e..dcff675eafc 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -9,10 +9,10 @@ (checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it). %li Specify the following URL during the Runner setup: - %code #{ci_root_url(only_path: false)} + %code= ci_root_url(only_path: false) %li Use the following registration token during setup: - %code #{@project.runners_token} + %code= @project.runners_token %li Start the Runner! diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index fc338dcf887..f1a80f1d5e1 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -17,4 +17,4 @@ - disabled_title = @service.disabled_title = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title - = link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel" + = link_to "Cancel", namespace_project_settings_integrations_path(@project.namespace, @project), class: "btn btn-cancel" diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/_index.html.haml index 66fd3029dc9..964133504e6 100644 --- a/app/views/projects/services/index.html.haml +++ b/app/views/projects/services/_index.html.haml @@ -1,5 +1,3 @@ -- page_title "Services" - .row.prepend-top-default.append-bottom-default .col-lg-3 %h4.prepend-top-0 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 63b797cd391..c1e576b42fc 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -8,8 +8,8 @@ by entering %code /<command_trigger_word> help - - unless enabled + - unless enabled || @service.template? = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service -- if enabled +- if enabled && !@service.template? = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service diff --git a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml index c929eee3bb9..fcc91be11cd 100644 --- a/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_installation_info.html.haml @@ -4,4 +4,4 @@ .col-sm-9.col-sm-offset-3 = link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do = custom_icon('mattermost_logo', size: 15) - = 'Add to Mattermost' + Add to Mattermost 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 c45052e3954..04b9100acc6 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -1,4 +1,5 @@ -- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" +- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path" +- run_actions_text = "Perform common operations on this project: #{pretty_name}" .well This service allows GitLab users to perform common operations on this @@ -9,85 +10,86 @@ %code /<command> help %br %br - To setup this service: - %ul.list-unstyled - %li - 1. - = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands' - in your Slack team with these options: + - unless @service.template? + To setup this service: + %ul.list-unstyled + %li + 1. + = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands' + in your Slack team with these options: - %hr + %hr - .help-form - .form-group - = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block - %p Fill in the word that works best for your team. - %p - Suggestions: - %code= 'gitlab' - %code= @project.path # Path contains no spaces, but dashes - %code= @project.path_with_namespace + .help-form + .form-group + = label_tag nil, 'Command', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.path_with_namespace - .form-group - = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#url') + .form-group + = label_tag :url, 'URL', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#url') - .form-group - = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block POST + .form-group + = label_tag nil, 'Method', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block POST - .form-group - = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#customize_name') + .form-group + = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :customize_name, 'GitLab', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#customize_name') - .form-group - = 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('gitlab_logo.png'), width: 36, height: 36) - = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank') + .form-group + = 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') - .form-group - = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.text-block Show this command in the autocomplete list - .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_description') + .form-group + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_description') - .form-group - = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#autocomplete_usage_hint') + .form-group + = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#autocomplete_usage_hint') - .form-group - = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label' - .col-sm-10.col-xs-12.input-group - = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly' - .input-group-btn - = clipboard_button(clipboard_target: '#descriptive_label') + .form-group + = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-xs-12 control-label' + .col-sm-10.col-xs-12.input-group + = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control input-sm', readonly: 'readonly' + .input-group-btn + = clipboard_button(clipboard_target: '#descriptive_label') - %hr + %hr - %ul.list-unstyled - %li - 2. Paste the - %strong Token - into the field below - %li - 3. Select the - %strong Active - checkbox, press - %strong Save changes - and start using GitLab inside Slack! + %ul.list-unstyled + %li + 2. Paste the + %strong Token + into the field below + %li + 3. Select the + %strong Active + checkbox, press + %strong Save changes + and start using GitLab inside Slack! diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml index ceabe2eab3d..ceabe2eab3d 100644 --- a/app/views/projects/hooks/_project_hook.html.haml +++ b/app/views/projects/settings/integrations/_project_hook.html.haml diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml new file mode 100644 index 00000000000..aa38a889cdd --- /dev/null +++ b/app/views/projects/settings/integrations/show.html.haml @@ -0,0 +1,3 @@ +- page_title 'Integrations' += render 'projects/hooks/index' += render 'projects/services/index' diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml new file mode 100644 index 00000000000..d81ed7bb609 --- /dev/null +++ b/app/views/projects/settings/members/show.html.haml @@ -0,0 +1,6 @@ +- page_title "Members" + += render "projects/project_members/index" +- if can?(current_user, :admin_project, @project) + - if @project.allowed_to_share_with_group? + = render "projects/group_links/index" diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml index d9d392fa02f..4ee30b023ac 100644 --- a/app/views/projects/stage/_graph.html.haml +++ b/app/views/projects/stage/_graph.html.haml @@ -1,6 +1,6 @@ - stage = local_assigns.fetch(:stage) - statuses = stage.statuses.latest -- status_groups = statuses.sort_by(&:name).group_by(&:group_name) +- status_groups = statuses.sort_by(&:sortable_name).group_by(&:group_name) %li.stage-column .stage-name %a{ name: stage.name } diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml index c4cb9ab50d0..9c5eb501174 100644 --- a/app/views/projects/stage/_in_stage_group.html.haml +++ b/app/views/projects/stage/_in_stage_group.html.haml @@ -5,9 +5,10 @@ %span.ci-status-text = name %span.dropdown-counter-badge= subject.size -.dropdown-menu.grouped-pipeline-dropdown + +%ul.dropdown-menu.big-pipeline-graph-dropdown-menu.js-grouped-pipeline-dropdown .arrow - %ul + .scrollable-menu - subject.each do |status| - %li.dropdown-build - = render 'ci/status/graph_badge', subject: status + %li + = render 'ci/status/dropdown_graph_badge', subject: status diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 038a960bd0c..6855c463c6d 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -7,10 +7,12 @@ %th.hidden-xs .pull-left Last commit .last-commit.hidden-sm.pull-left + %i.fa.fa-angle-right %small.light - = clipboard_button(clipboard_text: @commit.id) = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" + = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard") = time_ago_with_tooltip(@commit.committed_date) + \- = @commit.full_title %small.commit-history-link-spacer | = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'commit-history-link' diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index e25d6a48573..fb0efd85dcd 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -17,6 +17,13 @@ %pre.dark :preserve gem install gollum + %p + It is recommended to install + %code github-markdown + so that GFM features render locally: + %pre.dark + :preserve + gem install github-markdown %h3 Clone your wiki %pre.dark diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml index 05a63016c09..821a39d61f5 100644 --- a/app/views/search/results/_empty.html.haml +++ b/app/views/search/results/_empty.html.haml @@ -3,4 +3,4 @@ %h4 = icon('search') We couldn't find any results matching - %code #{@search_term} + %code= @search_term diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 07b17bc69c0..2e6adf3027c 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -2,7 +2,7 @@ %h4 = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title - .pull-right #{merge_request.to_reference} + .pull-right= merge_request.to_reference - if merge_request.description.present? .description.term = preserve do diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index c9b7bd154af..23ca6479414 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -9,7 +9,7 @@ = link_to user_snippets_path(snippet.author) do = image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' = snippet.author_name - %span.light #{time_ago_with_tooltip(snippet.created_at)} + %span.light= time_ago_with_tooltip(snippet.created_at) %h4.snippet-title - snippet_path = reliable_snippet_path(snippet) = link_to snippet_path do diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 027d42396b4..704d1d01a81 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -20,4 +20,4 @@ = link_to user_snippets_path(snippet_title.author) do = image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: '' = snippet_title.author_name - %span.light #{time_ago_with_tooltip(snippet_title.created_at)} + %span.light= time_ago_with_tooltip(snippet_title.created_at) diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml index 000532b1c9a..94295970acf 100644 --- a/app/views/shared/_choose_group_avatar_button.html.haml +++ b/app/views/shared/_choose_group_avatar_button.html.haml @@ -1,4 +1,4 @@ -%a.choose-btn.btn.btn-sm.js-choose-group-avatar-button +%button.choose-btn.btn.btn-sm.js-choose-group-avatar-button{ type: 'button' } %i.fa.fa-paperclip %span Choose File ... diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 96b75440309..03684389742 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -19,7 +19,7 @@ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true .input-group-btn - = clipboard_button(clipboard_target: '#project_clone') + = clipboard_button(clipboard_target: '#project_clone', title: "Copy URL to clipboard") :javascript $('ul.clone-options-dropdown a').on('click',function(e){ diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml index e7cb93b17a7..f94f8ffc604 100644 --- a/app/views/shared/_confirm_modal.html.haml +++ b/app/views/shared/_confirm_modal.html.haml @@ -14,7 +14,7 @@ To prevent accidental actions we ask you to confirm your intention. %br Please type - %code.js-confirm-danger-match #{phrase} + %code.js-confirm-danger-match= phrase to proceed or close this modal to cancel. .form-group diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index 39294fe1a09..704893b4d5b 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -6,14 +6,14 @@ = link_to milestones_filter_path(state: 'opened') do Open - if @project - %span.badge #{counts[:opened]} + %span.badge= counts[:opened] %li{ class: milestone_class_for_state(params[:state], 'closed') }> = link_to milestones_filter_path(state: 'closed') do Closed - if @project - %span.badge #{counts[:closed]} + %span.badge= counts[:closed] %li{ class: milestone_class_for_state(params[:state], 'all') }> = link_to milestones_filter_path(state: 'all') do All - if @project - %span.badge #{counts[:all]} + %span.badge= counts[:all] diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml index a43bf33751a..ed6fc76c61e 100644 --- a/app/views/shared/_no_password.html.haml +++ b/app/views/shared/_no_password.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_no_password_message].blank? && !current_user.hide_no_password && current_user.require_password? - .no-password-message.alert.alert-warning.hidden-xs + .no-password-message.alert.alert-warning You won't be able to pull or push project code via #{gitlab_config.protocol.upcase} until you #{link_to 'set a password', edit_profile_password_path} on your account - .pull-right + .alert-link-group = link_to "Don't show again", profile_path(user: {hide_no_password: true}), method: :put | = link_to 'Remind later', '#', class: 'hide-no-password-message' diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml index bb5fff2d3bb..d663fa13d10 100644 --- a/app/views/shared/_no_ssh.html.haml +++ b/app/views/shared/_no_ssh.html.haml @@ -1,8 +1,8 @@ - if cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key? - .no-ssh-key-message.alert.alert-warning.hidden-xs + .no-ssh-key-message.alert.alert-warning You won't be able to pull or push project code via SSH until you #{link_to 'add an SSH key', profile_keys_path, class: 'alert-link'} to your profile - .pull-right + .alert-link-group = link_to "Don't show again", profile_path(user: {hide_no_ssh_key: true}), method: :put, class: 'alert-link' | = link_to 'Remind later', '#', class: 'hide-no-ssh-message alert-link' diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index f9a7aa4e29b..dd9e433491b 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -32,7 +32,7 @@ - if group_member as - %span #{group_member.human_access} + %span= group_member.human_access - if group.description.present? .description diff --git a/app/views/shared/icons/_icon_stopwatch.svg b/app/views/shared/icons/_icon_stopwatch.svg new file mode 100644 index 00000000000..f20de04538e --- /dev/null +++ b/app/views/shared/icons/_icon_stopwatch.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg>
\ No newline at end of file diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index dcc1f3ba676..0a4de709fcd 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,4 +1,5 @@ - form = local_assigns.fetch(:f) +- commits = local_assigns[:commits] - project = @target_project || @project = form_errors(issuable) @@ -14,7 +15,7 @@ = form.label :title, class: 'control-label' = render 'shared/issuable/form/template_selector', issuable: issuable - = render 'shared/issuable/form/title', issuable: issuable, form: form + = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) = render 'shared/issuable/form/description', issuable: issuable, form: form @@ -67,7 +68,7 @@ - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) .inline.prepend-left-10 Please review the - %strong #{link_to 'contribution guidelines', guide_url} + %strong= link_to('contribution guidelines', guide_url) for this project. - if issuable.new_record? diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml new file mode 100644 index 00000000000..55360dadbc4 --- /dev/null +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -0,0 +1,133 @@ +- type = local_assigns.fetch(:type) + +.issues-filters + .issues-details-filters.row-content-block.second-block.filtered-search-block + = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do + - if params[:search].present? + = hidden_field_tag :search, params[:search] + - if @bulk_edit + .check-all-holder + = check_box_tag "check_all_issues", nil, false, + class: "check_all_issues left" + .issues-other-filters.filtered-search-container + .filtered-search-input-container + %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]) } + = icon('filter') + %button.clear-search.hidden{ type: 'button' } + = icon('times') + #js-dropdown-hint.dropdown-menu.hint-dropdown + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-action' => 'submit' } + %button.btn.btn-link + = icon('search') + %span + Keep typing and press Enter + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue + %i.fa{ class: "#{'{{icon}}'}" } + %span.js-filter-hint + {{hint}} + %span.js-filter-tag.dropdown-light-content + {{tag}} + #js-dropdown-author.dropdown-menu + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link.dropdown-user + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} + #js-dropdown-assignee.dropdown-menu + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-value' => 'none' } + %button.btn.btn-link + No Assignee + %li.divider + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link.dropdown-user + %img.avatar.avatar-inline{ 'data-src' => '{{avatar_url}}', alt: '{{name}}\'s avatar', width: '30' } + .dropdown-user-details + %span + {{name}} + %span.dropdown-light-content + @{{username}} + #js-dropdown-milestone.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-value' => 'none' } + %button.btn.btn-link + No Milestone + %li.filter-dropdown-item{ 'data-value' => 'upcoming' } + %button.btn.btn-link + Upcoming + %li.divider + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link.js-data-value + {{title}} + #js-dropdown-label.dropdown-menu{ 'data-dropdown' => true } + %ul{ 'data-dropdown' => true } + %li.filter-dropdown-item{ 'data-value' => 'none' } + %button.btn.btn-link + No Label + %li.divider + %ul.filter-dropdown{ 'data-dynamic' => true, 'data-dropdown' => true } + %li.filter-dropdown-item + %button.btn.btn-link + %span.dropdown-label-box{ style: 'background: {{color}}' } + %span.label-title.js-data-value + {{title}} + .pull-right + = render 'shared/sort_dropdown' + + - if @bulk_edit + .issues_bulk_update.hide + = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do + .filter-item.inline + = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do + %ul + %li + %a{ href: "#", data: { id: "reopen" } } Open + %li + %a{ href: "#", data: { id: "close" } } Closed + .filter-item.inline + = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", + placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) + .filter-item.inline + = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) + .filter-item.inline.labels-filter + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + .filter-item.inline + = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do + %ul + %li + %a{ href: "#", data: { id: "subscribe" } } Subscribe + %li + %a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe + + = hidden_field_tag 'update[issuable_ids]', [] + = hidden_field_tag :state_event, params[:state_event] + .filter-item.inline + = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save" + +:javascript + new UsersSelect(); + new LabelsSelect(); + new MilestoneSelect(); + new IssueStatusSelect(); + new SubscriptionSelect(); + + $(document).off('page:restore').on('page:restore', function (event) { + if (gl.FilteredSearchManager) { + new gl.FilteredSearchManager(); + } + Issuable.init(); + new gl.IssuableBulkActions({ + prefixId: 'issue_', + }); + }); diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index bc57d48ae7c..ec9bcaf63dd 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -1,5 +1,7 @@ - todo = issuable_todo(issuable) -%aside.right-sidebar{ class: sidebar_gutter_collapsed_class } +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('issuable/issuable_bundle.js') +%aside.right-sidebar{ 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 @@ -72,7 +74,13 @@ .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil = dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }}) - + - if issuable.has_attribute?(:time_estimate) + #issuable-time-tracker.block + %issuable-time-tracker{ ':time_estimate' => 'issuable.time_estimate', ':time_spent' => 'issuable.total_time_spent', ':human_time_estimate' => 'issuable.human_time_estimate', ':human_time_spent' => 'issuable.human_total_time_spent', 'stopwatch-svg' => custom_icon('icon_stopwatch'), 'docs-url' => help_page_path('workflow/time_tracking.md') } + // Fallback while content is loading + .title.hide-collapsed + Time tracking + = icon('spinner spin') - if issuable.has_attribute?(:due_date) .block.due_date .sidebar-collapsed-icon @@ -97,7 +105,7 @@ remove due date - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .selectbox.hide-collapsed - = f.hidden_field :due_date, value: issuable.due_date + = f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd') .dropdown %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } %span.dropdown-toggle-text Due date @@ -153,15 +161,17 @@ - project_ref = cross_project_reference(@project, issuable) .block.project-reference .sidebar-collapsed-icon.dont-change-state - = clipboard_button(clipboard_text: project_ref) + = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") .cross-project-reference.hide-collapsed %span Reference: %cite{ title: project_ref } = project_ref - = clipboard_button(clipboard_text: project_ref) + = clipboard_button(clipboard_text: project_ref, title: "Copy reference to clipboard", placement: "left") :javascript + gl.IssuableResource = new gl.SubbableResource('#{issuable_json_path(issuable)}'); + new gl.IssuableTimeTracking("#{escape_javascript(serialize_issuable(issuable))}"); new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}'); new LabelsSelect(); new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}'); diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml index 83efdc7c8f7..64826d41d60 100644 --- a/app/views/shared/issuable/form/_title.html.haml +++ b/app/views/shared/issuable/form/_title.html.haml @@ -1,4 +1,5 @@ - issuable = local_assigns.fetch(:issuable) +- has_wip_commits = local_assigns.fetch(:has_wip_commits) - form = local_assigns.fetch(:form) - no_issuable_templates = issuable_templates(issuable).empty? - div_class = no_issuable_templates ? 'col-sm-10' : 'col-sm-7 col-lg-8' @@ -18,6 +19,9 @@ %strong Work In Progress merge request to be merged when it's ready. .js-no-wip-explanation + - if has_wip_commits + It looks like you have some WIP commits in this branch. + %br %a.js-toggle-wip{ href: '', tabindex: -1 } Start the title with %code WIP: diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index a46ba3b0605..81b5bc1de30 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -37,7 +37,6 @@ %i.clear-icon.js-clear-input - if can_admin_member = link_to namespace_project_group_link_path(@project.namespace, @project, group_link), - remote: true, method: :delete, data: { confirm: "Are you sure you want to remove #{group.name}?" }, class: 'btn btn-remove prepend-left-10' do diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml index 51c195ffbcd..28935c8b598 100644 --- a/app/views/shared/milestones/_issuable.html.haml +++ b/app/views/shared/milestones/_issuable.html.haml @@ -16,7 +16,7 @@ = link_to_gfm issuable.title, [project.namespace.becomes(Namespace), project, issuable], title: issuable.title .issuable-detail = link_to [project.namespace.becomes(Namespace), project, issuable] do - %span.issuable-number >= issuable.to_reference + %span.issuable-number= issuable.to_reference - issuable.labels.each do |label| = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, label_name: label.title, state: 'all' }) do diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml index 15ff5b8a27e..31eb07ca666 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -8,7 +8,7 @@ = title - if show_counter .right - = issuables.size + = number_with_delimiter(issuables.size) - class_prefix = dom_class(issuables).pluralize %ul{ class: "well-list #{class_prefix}-sortable-list", id: "#{class_prefix}-list-#{id}", "data-state" => id } diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 3200aacf542..9e6a76e1ddb 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -9,7 +9,7 @@ .pull-right.light #{milestone.percent_complete(current_user)}% complete .row .col-sm-6 - = link_to pluralize(milestone.issues_visible_to_user(current_user).size, 'Issue'), issues_path + = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path · = link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path .col-sm-6= milestone_progress_bar(milestone) diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index 5074afb63a1..8bbaf431536 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -5,5 +5,5 @@ - scopes.each do |scope| %fieldset = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" - = label_tag "#{prefix}_scopes_#{scope}", scope - %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})" + = label_tag ("#{prefix}_scopes_#{scope}"), scope + %span= t(scope, scope: [:doorkeeper, :scopes]) diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 5d659eb83a9..13586a5a12a 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -1,6 +1,3 @@ -- page_title "Webhooks" -- context_title = @project ? 'project' : 'group' - .row.prepend-top-default .col-lg-3 %h4.prepend-top-0 diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index f51599212db..b09782749f5 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,6 +1,6 @@ %h4.prepend-top-20 Contributions for - %strong #{@calendar_date.to_s(:short)} + %strong= @calendar_date.to_s(:short) - if @events.any? %ul.bordered-list diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index fb25eed4f37..c3d33d49c1e 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -110,16 +110,16 @@ = spinner #groups.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX #contributed.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX #projects.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX #snippets.tab-pane - - # This tab is always loaded via AJAX + -# This tab is always loaded via AJAX .loading-status = spinner diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index 2badd0680fb..6abbb5a5250 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -2,6 +2,13 @@ class AuthorizedProjectsWorker include Sidekiq::Worker include DedicatedSidekiqQueue + # Schedules multiple jobs and waits for them to be completed. + def self.bulk_perform_and_wait(args_list) + job_ids = bulk_perform_async(args_list) + + Gitlab::JobWaiter.new(job_ids).wait + end + def self.bulk_perform_async(args_list) Sidekiq::Client.push_bulk('class' => self, 'args' => args_list) end diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb new file mode 100644 index 00000000000..fa9e097e40a --- /dev/null +++ b/app/workers/build_queue_worker.rb @@ -0,0 +1,10 @@ +class BuildQueueWorker + include Sidekiq::Worker + include BuildQueue + + def perform(build_id) + Ci::Build.find_by(id: build_id).try do |build| + Ci::UpdateBuildQueueService.new.execute(build) + end + end +end diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb index 9af9dae04f0..18b8daf4e1e 100644 --- a/app/workers/reactive_caching_worker.rb +++ b/app/workers/reactive_caching_worker.rb @@ -2,7 +2,7 @@ class ReactiveCachingWorker include Sidekiq::Worker include DedicatedSidekiqQueue - def perform(class_name, id) + def perform(class_name, id, *args) klass = begin Kernel.const_get(class_name) rescue NameError @@ -10,6 +10,6 @@ class ReactiveCachingWorker end return unless klass - klass.find_by(id: id).try(:exclusively_update_reactive_cache!) + klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args) end end diff --git a/app/workers/use_key_worker.rb b/app/workers/use_key_worker.rb new file mode 100644 index 00000000000..c9d382cc5d6 --- /dev/null +++ b/app/workers/use_key_worker.rb @@ -0,0 +1,13 @@ +class UseKeyWorker + include Sidekiq::Worker + include DedicatedSidekiqQueue + + def perform(key_id) + key = Key.find(key_id) + key.touch(:last_used_at) + rescue ActiveRecord::RecordNotFound + Rails.logger.error("UseKeyWorker: couldn't find key with ID=#{key_id}, skipping job") + + false + end +end |