diff options
489 files changed, 6387 insertions, 2201 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 6d4d7170fe8..d581610162f 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -62,7 +62,7 @@ Lint/UnusedMethodArgument: # Offense count: 93 # Configuration parameters: CountComments. Metrics/BlockLength: - Max: 288 + Enabled: false # Offense count: 3 # Cop supports --auto-correct. @@ -125,7 +125,7 @@ RSpec/MessageSpies: # Offense count: 3036 RSpec/MultipleExpectations: - Max: 37 + Enabled: false # Offense count: 2133 RSpec/NamedSubject: @@ -252,10 +252,9 @@ end group :development do gem 'foreman', '~> 0.78.0' - gem 'brakeman', '~> 3.3.0', require: false + gem 'brakeman', '~> 3.4.0', require: false gem 'letter_opener_web', '~> 1.3.0' - gem 'rerun', '~> 0.11.0' gem 'bullet', '~> 5.2.0', require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'web-console', '~> 2.0' @@ -286,7 +285,7 @@ group :development, :test do gem 'minitest', '~> 5.7.0' # Generate Fake data - gem 'ffaker', '~> 2.0.0' + gem 'ffaker', '~> 2.4' gem 'capybara', '~> 2.6.2' gem 'capybara-screenshot', '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index e2d7f94e571..104e6444803 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -88,7 +88,7 @@ GEM bootstrap-sass (3.3.6) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) - brakeman (3.3.2) + brakeman (3.4.1) browser (2.2.0) builder (3.2.2) bullet (5.2.0) @@ -198,7 +198,7 @@ GEM faraday_middleware-multi_json (0.0.6) faraday_middleware multi_json - ffaker (2.0.0) + ffaker (2.4.0) ffi (1.9.10) flay (2.6.1) ruby_parser (~> 3.0) @@ -407,9 +407,6 @@ GEM xml-simple licensee (8.0.0) rugged (>= 0.24b) - listen (3.0.5) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) little-plugger (1.1.4) logging (2.1.0) little-plugger (~> 1.1) @@ -580,9 +577,6 @@ GEM rainbow (2.1.0) raindrops (0.17.0) rake (10.5.0) - rb-fsevent (0.9.6) - rb-inotify (0.9.5) - ffi (>= 0.5.0) rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) rdoc (4.2.2) @@ -611,8 +605,6 @@ GEM redis-store (1.2.0) redis (>= 2.2) request_store (1.3.1) - rerun (0.11.0) - listen (~> 3.0) responders (2.3.0) railties (>= 4.2.0, < 5.1) rest-client (2.0.0) @@ -852,7 +844,7 @@ DEPENDENCIES better_errors (~> 1.0.1) binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) - brakeman (~> 3.3.0) + brakeman (~> 3.4.0) browser (~> 2.2) bullet (~> 5.2.0) bundler-audit (~> 0.5.0) @@ -877,7 +869,7 @@ DEPENDENCIES email_reply_trimmer (~> 0.1) email_spec (~> 1.6.0) factory_girl_rails (~> 4.7.0) - ffaker (~> 2.0.0) + ffaker (~> 2.4) flay (~> 2.6.1) fog-aws (~> 0.9) fog-core (~> 1.40) @@ -968,7 +960,6 @@ DEPENDENCIES redis-namespace (~> 1.5.2) redis-rails (~> 5.0.1) request_store (~> 1.3) - rerun (~> 0.11.0) responders (~> 2.0) rouge (~> 2.0) rqrcode-rails3 (~> 0.1.7) 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/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 8d1a0f482f9..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 */ @@ -88,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); 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 d5859444a32..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,7 @@ 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', @@ -31,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 fca47002870..0df84234520 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,9 +1,9 @@ -/* 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'; @@ -70,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 @@ -85,7 +85,7 @@ }; Build.prototype.getInitialBuildTrace = function() { - var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped'] + var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']; return $.ajax({ url: this.buildUrl, @@ -153,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 @@ -291,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/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..cabeae74ae3 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,4 +1,4 @@ -/* 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() { @@ -58,7 +58,5 @@ }; 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 251f2ded347..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) { @@ -66,7 +65,5 @@ }; 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_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_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 99a34651639..dcf67a8fd68 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 */ @@ -372,7 +372,5 @@ }; return Dispatcher; - })(); - }).call(this); 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 201f9fdc3fe..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; @@ -135,7 +135,6 @@ return selectedDateValue.length ? $('.js-remove-due-date-holder').removeClass('hidden') : $('.js-remove-due-date-holder').addClass('hidden'); - } }).done((data) => { if (isDropdown) { @@ -179,5 +178,4 @@ } global.DueDateSelectors = DueDateSelectors; - })(window.gl || (window.gl = {})); 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/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/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 6ca543c2b00..a1b7b442882 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,8 +48,9 @@ }, DefaultOptions: { sorter: function(query, items, searchKey) { - this.setting.highlightFirst = query.length > 0; + 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); @@ -153,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) }; @@ -334,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]; @@ -371,5 +372,4 @@ return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0]; } }; - }).call(this); diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 00859728c30..7746535d9ed 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; @@ -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, '\\\'') + "']"); } @@ -695,8 +693,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 +800,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')); @@ -829,7 +827,6 @@ }; 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 04814fa0843..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 */ @@ -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..5247b2a08f7 100644 --- a/app/assets/javascripts/group_avatar.js +++ b/app/assets/javascripts/group_avatar.js @@ -1,4 +1,4 @@ -/* 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() { @@ -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..9c53cdee58e 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -1,4 +1,4 @@ -/* 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 */ (function() { @@ -66,7 +66,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..c260ad03d47 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_'; @@ -163,5 +162,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..fd1e229e30a 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,7 +333,7 @@ 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')) { @@ -484,5 +484,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/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.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index eefb727f009..e2503ca305c 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ 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; @@ -216,5 +216,4 @@ return Array.prototype.indexOf.call(matchingNodes, node) !== -1; }; })(window); - }).call(this); 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 c856a26ae40..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,7 +11,7 @@ } 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); }; @@ -159,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..4620715a521 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 @@ -119,11 +119,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 +144,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 +178,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/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_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 0305aeb07d9..7a315e43667 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) { @@ -187,9 +187,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 +205,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 +247,5 @@ }; return MergeRequestWidget; - })(); - })(window.gl || (window.gl = {})); 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/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 603db88567d..06a72efa21d 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; @@ -244,6 +245,16 @@ }; + 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. @@ -429,6 +440,7 @@ */ Notes.prototype.addNote = function(xhr, note, status) { + this.handleCreateChanges(note); return this.renderNote(note); }; @@ -508,7 +520,7 @@ } return isAllowed; - } + }; /* @@ -903,7 +915,7 @@ $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); @@ -917,7 +929,7 @@ }; 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() { @@ -962,7 +974,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/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..6dbaae25f2a 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); @@ -95,5 +94,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 5945cab4cf0..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() { @@ -96,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 cec8856d4e7..480755899fb 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, @@ -105,7 +104,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) { @@ -216,7 +215,7 @@ this.dropdown.addClass('open').trigger('shown.bs.dropdown'); return this.searchInput.removeClass('disabled'); } - }; + } // Saves last length of the entered text onSearchInputKeyDown() { @@ -280,12 +279,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 @@ -305,12 +304,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]); } @@ -331,7 +330,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') { @@ -361,7 +360,7 @@ 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) { @@ -384,8 +383,7 @@ this.disableAutocomplete(); return this.searchInput.val('').focus(); } - }; - + } } global.SearchAutocomplete = SearchAutocomplete; @@ -426,5 +424,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 97fa68c8437..28491934c10 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 */ @@ -86,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 9602526063e..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, prefer-arrow-callback, 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; @@ -86,7 +86,6 @@ }; return SingleFileDiff; - })(); $.fn.singleFileDiff = function() { @@ -96,5 +95,4 @@ } }); }; - }).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 bdec948fb63..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') }; @@ -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/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..3ba70e7b439 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) { @@ -114,7 +114,5 @@ }; return U2FAuthenticate; - })(); - })(); diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index bb9942a3aa0..499a24f58df 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -1,8 +1,8 @@ -/* 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) { @@ -23,7 +23,5 @@ }; return U2FError; - })(); - }).call(this); diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 050c9bfc02e..87c1f5ff62c 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) { @@ -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..7ffc546ffc1 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); @@ -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/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_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index f075a995846..32973132174 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -5,18 +5,19 @@ gl.VueStage = Vue.extend({ data() { return { - count: 0, builds: '', spinner: '<span class="fa fa-spinner fa-spin"></span>', }; }, props: ['stage', 'svgs', 'match'], methods: { - fetchBuilds() { - if (this.count > 0) return null; + 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.count += 1; this.builds = JSON.parse(response.body).html; }, () => { const flash = new Flash('Something went wrong on our end.'); @@ -39,7 +40,7 @@ return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`; }, svg() { - const icon = this.stage.status.icon; + const { icon } = this.stage.status; const stageIcon = icon.replace(/icon/i, 'stage_icon'); return this.svgs[this.match(stageIcon)]; }, @@ -50,18 +51,25 @@ template: ` <div> <button - @click='fetchBuilds' + @click='fetchBuilds($event)' :class="triggerButtonClass" :title='stage.title' data-placement="top" data-toggle="dropdown" - type="button"> + type="button" + > <span v-html="svg"></span> <i class="fa fa-caret-down "></i> </button> <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> <div class="arrow-up"></div> - <div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" v-html="buildsOrSpinner"></div> + <div + @click='' + :class="dropdownClass" + class="js-builds-dropdown-list scrollable-menu" + v-html="buildsOrSpinner" + > + </div> </ul> </div> `, 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/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index e04a87a7327..bb6129158d9 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -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; } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index fee38b05023..d957ec64654 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -76,7 +76,7 @@ .filter-dropdown { max-height: 215px; - overflow-x: scroll; + overflow: auto; } .filter-dropdown-item { @@ -86,7 +86,7 @@ text-align: left; padding: 8px 16px; text-overflow: ellipsis; - overflow-y: hidden; + overflow: hidden; border-radius: 0; .fa { diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 838f5442fff..f0b03710c79 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -236,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/variables.scss b/app/assets/stylesheets/framework/variables.scss index 349cd9c189e..07cb669a46e 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; @@ -280,6 +282,7 @@ $dropdown-hover-color: #3b86ff; */ $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; +$btn-white-active: #848484; /* * Badges @@ -433,6 +436,7 @@ $help-shortcut-header-color: #333; */ $issues-today-bg: #f3fff2; $issues-today-border: #e1e8d5; +$compare-display-color: #888; /* * jQuery UI diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 0ae5dc5c537..324c6cec96a 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -473,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/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 8861315d776..8dff22e32bd 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -44,8 +44,8 @@ .pipeline-info, .pipeline-commit, - .pipeline-actions, - .pipeline-stages { + .pipeline-stages, + .pipeline-actions { width: 20%; } } @@ -185,6 +185,7 @@ .stage-cell { font-size: 0; + padding: 10px 4px; > .stage-container > div > button > span > svg, > .stage-container > button > svg { @@ -202,8 +203,8 @@ position: relative; margin-right: 6px; - .tooltip { - white-space: nowrap; + .tooltip-inner { + padding: 3px 4px; } &:not(:last-child) { @@ -348,6 +349,7 @@ padding: $gl-padding; white-space: nowrap; transition: max-height 0.3s, padding 0.3s; + overflow: auto; .stage-column-list, .builds-container > ul { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 9455ba3b98a..131cf23299a 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -9,17 +9,17 @@ .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 { @@ -905,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 { diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 5f13353baa1..6db4e1dc1bc 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -18,7 +18,7 @@ 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 = [current_user, *@users] end if params[:author_id].present? 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/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 7051652d109..7f506db583f 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -19,11 +19,11 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController private def milestones - @milestones = GlobalMilestone.build_collection(@projects, params) + @milestones = DashboardMilestone.build_collection(@projects, params) end def milestone - @milestone = GlobalMilestone.build(@projects, params[:title]) + @milestone = DashboardMilestone.build(@projects, params[:title]) render_404 unless @milestone end end diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index fbe391fc58c..9b45ed6b6af 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(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/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index aaebd4efa00..9ac5bf4b9f8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -347,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 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/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/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/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/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/models/ci/build.rb b/app/models/ci/build.rb index 27042798741..ce23c2d1088 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 @@ -507,6 +508,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/commit.rb b/app/models/commit.rb index 0b924b063a4..5d942cb0422 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -318,6 +318,20 @@ 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, full: false) 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/milestoneish.rb b/app/models/concerns/milestoneish.rb index fcc8feddb39..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 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/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/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 c9910d8cd09..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(#{Gitlab.config.git.bin_path} --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/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/merge_request.rb b/app/models/merge_request.rb index 70005a87f4b..cd5b345bae5 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?] @@ -898,10 +902,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 64dd586c9e0..dadb81f9b6e 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -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/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/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/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..3edd63451e7 --- /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, 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(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) + +%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/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/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/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 0a9563ed7e7..51d5d7563fc 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 @@ -136,6 +137,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| 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..cdd765c85eb 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -1,6 +1,8 @@ module Notes class CreateService < BaseService def execute + merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha) + note = project.notes.new(params) note.author = current_user note.system = false @@ -19,7 +21,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/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb index 2edbd39a9e7..aaea9717fc4 100644 --- a/app/services/notes/slash_commands_service.rb +++ b/app/services/notes/slash_commands_service.rb @@ -19,10 +19,10 @@ module Notes 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/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/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index 21ec1bd9e65..2d211d5ebbe 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 @@ -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. 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/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/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/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 12e4280d344..990908211de 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, @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..08eb0c57f66 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -1,7 +1,7 @@ .page-content-header .header-main-content %strong - = clipboard_button(clipboard_text: @commit.id) + = clipboard_button(clipboard_text: @commit.id, title: "Copy commit SHA to clipboard") = @commit.short_id %span.hidden-xs authored #{time_ago_with_tooltip(@commit.authored_date)} 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 3c6c50dce3c..002e3d345dc 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -36,6 +36,6 @@ .table-list-cell.commit-actions.hidden-xs - if commit.status(ref) = render_commit_status(commit, ref: ref) - = clipboard_button(clipboard_text: commit.id) + = 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/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/edit.html.haml b/app/views/projects/edit.html.haml index 38e7fc4279c..1d7fdf68cb3 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -21,76 +21,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/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 43141971231..9fa00811af0 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -2,6 +2,8 @@ - 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/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 34ead6427e0..36c6e7a8dad 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -11,7 +11,7 @@ = 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 diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 110dd11d1ce..2a7cd3a19d0 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -3,6 +3,7 @@ - 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) } @@ -112,3 +113,6 @@ 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/_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/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/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/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 6d7c2defe2b..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('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, '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/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml index 038a960bd0c..2c08221565b 100644 --- a/app/views/projects/tree/_tree_content.html.haml +++ b/app/views/projects/tree/_tree_content.html.haml @@ -8,7 +8,7 @@ .pull-left Last commit .last-commit.hidden-sm.pull-left %small.light - = clipboard_button(clipboard_text: @commit.id) + = 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: "monospace" = time_ago_with_tooltip(@commit.committed_date) = @commit.full_title 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..c0e8a498316 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 diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index a02b815e3cd..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 @@ -162,6 +170,8 @@ = 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/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 c8fd45c4319..31eb07ca666 100644 --- a/app/views/shared/milestones/_issuables.html.haml +++ b/app/views/shared/milestones/_issuables.html.haml @@ -8,8 +8,7 @@ = title - if show_counter .right - = issuables.size - .pull-right= number_with_delimiter(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/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml b/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml new file mode 100644 index 00000000000..268be6b9b83 --- /dev/null +++ b/changelogs/unreleased/23524-notify-automerge-user-of-failed-build.yml @@ -0,0 +1,4 @@ +--- +title: Create a TODO for user who set auto-merge when a build fails, merge conflict occurs +merge_request: 8056 +author: twonegatives diff --git a/changelogs/unreleased/24032-changed-visibility-level-to-public-but-project-is-not-public.yml b/changelogs/unreleased/24032-changed-visibility-level-to-public-but-project-is-not-public.yml new file mode 100644 index 00000000000..875106d7dd5 --- /dev/null +++ b/changelogs/unreleased/24032-changed-visibility-level-to-public-but-project-is-not-public.yml @@ -0,0 +1,4 @@ +--- +title: Updated project visibility settings UX +merge_request: 7645 +author: diff --git a/changelogs/unreleased/24915_merge_slash_command.yml b/changelogs/unreleased/24915_merge_slash_command.yml new file mode 100644 index 00000000000..eb8ced8ab01 --- /dev/null +++ b/changelogs/unreleased/24915_merge_slash_command.yml @@ -0,0 +1,4 @@ +--- +title: Support slash comand `/merge` for merging merge requests. +merge_request: 7746 +author: Jarka Kadlecova diff --git a/changelogs/unreleased/26117-sort-pipeline-for-commit.yml b/changelogs/unreleased/26117-sort-pipeline-for-commit.yml new file mode 100644 index 00000000000..b2f5294d380 --- /dev/null +++ b/changelogs/unreleased/26117-sort-pipeline-for-commit.yml @@ -0,0 +1,4 @@ +--- +title: Add sorting pipeline for a commit +merge_request: 8319 +author: Takuya Noguchi diff --git a/changelogs/unreleased/26667-pipeline-width-for-huge-pipeline.yml b/changelogs/unreleased/26667-pipeline-width-for-huge-pipeline.yml new file mode 100644 index 00000000000..08dcc5c3e8c --- /dev/null +++ b/changelogs/unreleased/26667-pipeline-width-for-huge-pipeline.yml @@ -0,0 +1,4 @@ +--- +title: Fixes big pipeline and small pipeline width problems and tooltips text being outside the tooltip +merge_request: 8593 +author: diff --git a/changelogs/unreleased/26773-fix-project-statistics-repository-size.yml b/changelogs/unreleased/26773-fix-project-statistics-repository-size.yml new file mode 100644 index 00000000000..8ce9bbcb3a9 --- /dev/null +++ b/changelogs/unreleased/26773-fix-project-statistics-repository-size.yml @@ -0,0 +1,4 @@ +--- +title: Adjust ProjectStatistic#repository_size with values saved as MB +merge_request: 8616 +author: diff --git a/changelogs/unreleased/8623-correct-robots-txt.yml b/changelogs/unreleased/8623-correct-robots-txt.yml new file mode 100644 index 00000000000..00ed80511cc --- /dev/null +++ b/changelogs/unreleased/8623-correct-robots-txt.yml @@ -0,0 +1,4 @@ +--- +title: "Correct User-agent placement in robots.txt" +merge_request: 8623 +author: Eric Sabelhaus diff --git a/changelogs/unreleased/clipboard-button-commit-sha.yml b/changelogs/unreleased/clipboard-button-commit-sha.yml new file mode 100644 index 00000000000..6aa4a5664e7 --- /dev/null +++ b/changelogs/unreleased/clipboard-button-commit-sha.yml @@ -0,0 +1,3 @@ +--- +title: 'Copy commit SHA to clipboard' +merge_request: 8547 diff --git a/changelogs/unreleased/fix-external-status-badge-links.yml b/changelogs/unreleased/fix-external-status-badge-links.yml new file mode 100644 index 00000000000..2287a9b76c4 --- /dev/null +++ b/changelogs/unreleased/fix-external-status-badge-links.yml @@ -0,0 +1,4 @@ +--- +title: Link external build badge to its target URL +merge_request: 8611 +author: diff --git a/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml b/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml new file mode 100644 index 00000000000..3d8cf1c74a2 --- /dev/null +++ b/changelogs/unreleased/fix-keep-artifacts-button-visibility.yml @@ -0,0 +1,4 @@ +--- +title: Hide build artifacts keep button if operation is not allowed +merge_request: 8501 +author: diff --git a/changelogs/unreleased/i--25814-500-error.yml b/changelogs/unreleased/i--25814-500-error.yml new file mode 100644 index 00000000000..cd55ede84c8 --- /dev/null +++ b/changelogs/unreleased/i--25814-500-error.yml @@ -0,0 +1,4 @@ +--- +title: Fix Compare page throws 500 error when any branch/reference is not selected +merge_request: 8492 +author: Martin Cabrera diff --git a/changelogs/unreleased/input-button-hover.yml b/changelogs/unreleased/input-button-hover.yml new file mode 100644 index 00000000000..cbb35adb769 --- /dev/null +++ b/changelogs/unreleased/input-button-hover.yml @@ -0,0 +1,4 @@ +--- +title: Add hover state to MR comment reply button +merge_request: +author: diff --git a/changelogs/unreleased/issue_25017.yml b/changelogs/unreleased/issue_25017.yml new file mode 100644 index 00000000000..09126ae81bc --- /dev/null +++ b/changelogs/unreleased/issue_25017.yml @@ -0,0 +1,4 @@ +--- +title: Show 'too many changes' message for created merge requests when they are too large +merge_request: +author: diff --git a/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml b/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml new file mode 100644 index 00000000000..b8c7b78cf0d --- /dev/null +++ b/changelogs/unreleased/mr-tabs-alignment-sidebar-open.yml @@ -0,0 +1,4 @@ +--- +title: Fixed merge request tabs dont move when opening collapsed sidebar +merge_request: +author: diff --git a/changelogs/unreleased/reduce-queries-milestone-index.yml b/changelogs/unreleased/reduce-queries-milestone-index.yml new file mode 100644 index 00000000000..a779b58c973 --- /dev/null +++ b/changelogs/unreleased/reduce-queries-milestone-index.yml @@ -0,0 +1,4 @@ +--- +title: Use cached values to compute total issues count in milestone index pages +merge_request: 8518 +author: diff --git a/changelogs/unreleased/refresh-authorizations-tighter-lease.yml b/changelogs/unreleased/refresh-authorizations-tighter-lease.yml new file mode 100644 index 00000000000..ab42b2eb72d --- /dev/null +++ b/changelogs/unreleased/refresh-authorizations-tighter-lease.yml @@ -0,0 +1,4 @@ +--- +title: Synchronize all project authorization refreshing work to prevent race conditions +merge_request: +author: diff --git a/changelogs/unreleased/speed-up-dashboard-milestone-index.yml b/changelogs/unreleased/speed-up-dashboard-milestone-index.yml new file mode 100644 index 00000000000..ba4ff931ea8 --- /dev/null +++ b/changelogs/unreleased/speed-up-dashboard-milestone-index.yml @@ -0,0 +1,5 @@ +--- +title: Speed up dashboard milestone index by scoping IssuesFinder to user authorized + projects +merge_request: 8524 +author: diff --git a/changelogs/unreleased/time-tracking-api.yml b/changelogs/unreleased/time-tracking-api.yml new file mode 100644 index 00000000000..b58d73bef81 --- /dev/null +++ b/changelogs/unreleased/time-tracking-api.yml @@ -0,0 +1,4 @@ +--- +title: Add new endpoints for Time Tracking. +merge_request: 8483 +author: diff --git a/changelogs/unreleased/wip-mr-from-commits.yml b/changelogs/unreleased/wip-mr-from-commits.yml new file mode 100644 index 00000000000..0083798be08 --- /dev/null +++ b/changelogs/unreleased/wip-mr-from-commits.yml @@ -0,0 +1,4 @@ +--- +title: Mark MR as WIP when pushing WIP commits +merge_request: 8124 +author: Jurre Stender @jurre diff --git a/config/application.rb b/config/application.rb index 8ce549cebf6..f00e58a36ca 100644 --- a/config/application.rb +++ b/config/application.rb @@ -88,6 +88,7 @@ module Gitlab config.assets.precompile << "print.css" config.assets.precompile << "notify.css" config.assets.precompile << "mailers/*.css" + config.assets.precompile << "lib/vue_resource.js" config.assets.precompile << "katex.css" config.assets.precompile << "katex.js" config.assets.precompile << "xterm/xterm.css" @@ -98,6 +99,7 @@ module Gitlab config.assets.precompile << "protected_branches/protected_branches_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "merge_request_widget/ci_bundle.js" + config.assets.precompile << "issuable/issuable_bundle.js" config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" diff --git a/config/routes/project.rb b/config/routes/project.rb index 26e2dc9e6e7..1fc6ed28c74 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -94,6 +94,7 @@ constraints(ProjectUrlConstrainer.new) do get :pipelines get :merge_check post :merge + get :merge_widget_refresh post :cancel_merge_when_build_succeeds get :ci_status get :ci_environments_status diff --git a/db/migrate/20161223034433_add_time_estimate_to_issuables.rb b/db/migrate/20161223034433_add_time_estimate_to_issuables.rb new file mode 100644 index 00000000000..8d89756a9bc --- /dev/null +++ b/db/migrate/20161223034433_add_time_estimate_to_issuables.rb @@ -0,0 +1,30 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddTimeEstimateToIssuables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + add_column :issues, :time_estimate, :integer + add_column :merge_requests, :time_estimate, :integer + end +end diff --git a/db/migrate/20161223034646_create_timelogs.rb b/db/migrate/20161223034646_create_timelogs.rb new file mode 100644 index 00000000000..d3353a67eec --- /dev/null +++ b/db/migrate/20161223034646_create_timelogs.rb @@ -0,0 +1,38 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateTimelogs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + create_table :timelogs do |t| + t.integer :time_spent, null: false + t.references :trackable, polymorphic: true + t.references :user + + t.timestamps null: false + end + + add_index :timelogs, [:trackable_type, :trackable_id] + add_index :timelogs, :user_id + end +end diff --git a/db/schema.rb b/db/schema.rb index c58a886b0fa..7815392c1c3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -506,6 +506,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do t.integer "lock_version" t.text "title_html" t.text "description_html" + t.integer "time_estimate" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -685,6 +686,7 @@ ActiveRecord::Schema.define(version: 20170106172224) do t.integer "lock_version" t.text "title_html" t.text "description_html" + t.integer "time_estimate" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -1128,6 +1130,18 @@ ActiveRecord::Schema.define(version: 20170106172224) do add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree + create_table "timelogs", force: :cascade do |t| + t.integer "time_spent", null: false + t.integer "trackable_id" + t.string "trackable_type" + t.integer "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree + add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree + create_table "todos", force: :cascade do |t| t.integer "user_id", null: false t.integer "project_id", null: false diff --git a/doc/api/issues.md b/doc/api/issues.md index dd84afd7c73..b276d1ad918 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -712,6 +712,146 @@ Example response: } ``` +## Set a time estimate for an issue + +Sets an estimated time of work for this issue. + +``` +POST /projects/:id/issues/:issue_id/time_estimate +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_estimate?duration=3h30m +``` + +Example response: + +```json +{ + "human_time_estimate": "3h 30m", + "human_total_time_spent": null, + "time_estimate": 12600, + "total_time_spent": 0 +} +``` + +## Reset the time estimate for an issue + +Resets the estimated time for this issue to 0 seconds. + +``` +POST /projects/:id/issues/:issue_id/reset_time_estimate +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_time_estimate +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": null, + "time_estimate": 0, + "total_time_spent": 0 +} +``` + +## Add spent time for an issue + +Adds spent time for this issue + +``` +POST /projects/:id/issues/:issue_id/add_spent_time +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/add_spent_time?duration=1h +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": "1h", + "time_estimate": 0, + "total_time_spent": 3600 +} +``` + +## Reset spent time for an issue + +Resets the total spent time for this issue to 0 seconds. + +``` +POST /projects/:id/issues/:issue_id/reset_spent_time +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/reset_spent_time +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": null, + "time_estimate": 0, + "total_time_spent": 0 +} +``` + +## Get time tracking stats + +``` +GET /projects/:id/issues/:issue_id/time_stats +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `issue_id` | integer | yes | The ID of a project's issue | + +```bash +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/time_stats +``` + +Example response: + +```json +{ + "human_time_estimate": "2h", + "human_total_time_spent": "1h", + "time_estimate": 7200, + "total_time_spent": 3600 +} +``` + ## Comments on issues Comments are done via the [notes](notes.md) resource. diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 662cc9da733..7b005591545 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -1018,3 +1018,142 @@ Example response: }] } ``` +## Set a time estimate for a merge request + +Sets an estimated time of work for this merge request. + +``` +POST /projects/:id/merge_requests/:merge_request_id/time_estimate +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a project's merge request | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_estimate?duration=3h30m +``` + +Example response: + +```json +{ + "human_time_estimate": "3h 30m", + "human_total_time_spent": null, + "time_estimate": 12600, + "total_time_spent": 0 +} +``` + +## Reset the time estimate for a merge request + +Resets the estimated time for this merge request to 0 seconds. + +``` +POST /projects/:id/merge_requests/:merge_request_id/reset_time_estimate +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a project's merge_request | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_time_estimate +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": null, + "time_estimate": 0, + "total_time_spent": 0 +} +``` + +## Add spent time for a merge request + +Adds spent time for this merge request + +``` +POST /projects/:id/merge_requests/:merge_request_id/add_spent_time +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a project's merge request | +| `duration` | string | yes | The duration in human format. e.g: 3h30m | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/add_spent_time?duration=1h +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": "1h", + "time_estimate": 0, + "total_time_spent": 3600 +} +``` + +## Reset spent time for a merge request + +Resets the total spent time for this merge request to 0 seconds. + +``` +POST /projects/:id/merge_requests/:merge_request_id/reset_spent_time +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a project's merge_request | + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/reset_spent_time +``` + +Example response: + +```json +{ + "human_time_estimate": null, + "human_total_time_spent": null, + "time_estimate": 0, + "total_time_spent": 0 +} +``` + +## Get time tracking stats + +``` +GET /projects/:id/merge_requests/:merge_request_id/time_stats +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer | yes | The ID of a project | +| `merge_request_id` | integer | yes | The ID of a project's merge request | + +```bash +curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/93/time_stats +``` + +Example response: + +```json +{ + "human_time_estimate": "2h", + "human_total_time_spent": "1h", + "time_estimate": 7200, + "total_time_spent": 3600 +} +``` diff --git a/doc/development/frontend.md b/doc/development/frontend.md index 9e782ab977f..f79bd23dc90 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -297,16 +297,74 @@ For our currently-supported browsers, see our [requirements][requirements]. ## Gotchas -### Phantom.JS (used by Teaspoon & Rspec) chokes, returning vague JavaScript errors - -If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being thrown in tests, but -can't reproduce them manually, you may have included `ES6`-style JavaScript in files that don't -have the `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file you're -working in (`git mv <file.js> <file.js.es6>`). - -Similar errors will be thrown if you're using -any of the [array methods introduced in ES6](http://www.2ality.com/2014/05/es6-array-methods.html) -whether or not you've updated the file extension. +### Spec errors due to use of ES6 features in `.js` files + +If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being +thrown in Teaspoon, Spinach, or Rspec tests but can't reproduce them manually, +you may have included `ES6`-style JavaScript in files that don't have the +`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file +you're working in (`git mv <file.js> <file.js.es6>`). + +### Spec errors due to use of unsupported JavaScript + +Similar errors will be thrown if you're using JavaScript features not yet +supported by our test runner's version of webkit, whether or not you've updated +the file extension. Examples of unsupported JavaScript features are: + +- Array.from +- Array.find +- Array.first +- Object.assign +- Async functions +- Generators +- Array destructuring +- For Of +- Symbol/Symbol.iterator +- Spread + +Until these are polyfilled or transpiled appropriately, they should not be used. +Please update this list with additional unsupported features or when any of +these are made usable. + +### Spec errors due to JavaScript not enabled + +If, as a result of a change you've made, a feature now depends on JavaScript to +run correctly, you need to make sure a JavaScript web driver is enabled when +specs are run. If you don't you'll see vague error messages from the spec +runner, and an explosion of vague console errors in the HTML snapshot. + +To enable a JavaScript driver in an `rspec` test, add `js: true` to the +individual spec or the context block containing multiple specs that need +JavaScript enabled: + +```ruby + +# For one spec +it 'presents information about abuse report', js: true do + # assertions... +end + +describe "Admin::AbuseReports", js: true do + it 'presents information about abuse report' do + # assertions... + end + it 'shows buttons for adding to abuse report' do + # assertions... + end +end +``` +In Spinach, the JavaScript driver is enabled differently. In the `*.feature` +file for the failing spec, add the `@javascript` flag above the Scenario: +``` +@javascript +Scenario: Developer can approve merge request + Given I am a "Shop" developer + And I visit project "Shop" merge requests page + And merge request 'Bug NS-04' must be approved + And I click link "Bug NS-04" + When I click link "Approve" + Then I should see approved merge request "Bug NS-04" +``` diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 4c933cef9b7..98a680d0dbe 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -41,6 +41,9 @@ that are in common for all providers that we need to consider. - `allow_single_sign_on` allows you to specify the providers you want to allow to automatically create an account. It defaults to `false`. If `false` users must be created manually or they will not be able to sign in via OmniAuth. +- `auto_link_ldap_user` can be used if you have [LDAP / ActiveDirectory](ldap.md) + integration enabled. It defaults to false. When enabled, users automatically + created through OmniAuth will be linked to their LDAP entry as well. - `block_auto_created_users` defaults to `true`. If `true` auto created users will be blocked by default and will have to be unblocked by an administrator before they are able to sign in. @@ -52,6 +55,10 @@ SAML, Shibboleth, Crowd or Google, or set it to `false` otherwise any user on the Internet will be able to successfully sign in to your GitLab without administrative approval. +>**Note:** +`auto_link_ldap_user` requires the `uid` of the user to be the same in both LDAP +and the OmniAuth provider. + To change these settings: * **For omnibus package** @@ -72,6 +79,7 @@ To change these settings: # using an array, e.g. ["saml", "twitter"], or as true/false to allow all providers or none. # User accounts will be created automatically when authentication was successful. gitlab_rails['omniauth_allow_single_sign_on'] = ['saml', 'twitter'] + gitlab_rails['omniauth_auto_link_ldap_user'] = true gitlab_rails['omniauth_block_auto_created_users'] = true ``` @@ -99,6 +107,8 @@ To change these settings: # User accounts will be created automatically when authentication was successful. allow_single_sign_on: ["saml", "twitter"] + auto_link_ldap_user: true + # Locks down those users until they have been cleared by the admin (default: true). block_auto_created_users: true ``` diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 0f398874b8f..547d855d777 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -46,6 +46,7 @@ further configuration instructions and details. Contributions are welcome. | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | | [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | | [Slack Notifications](slack.md) | Receive event notifications in Slack | +| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands | | PivotalTracker | Project Management Software (Source Commits Endpoint) | | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | | [Redmine](redmine.md) | Redmine issue tracker | diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index 5f6a6c6503e..a6546cffce2 100644 --- a/doc/user/project/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -14,6 +14,7 @@ do. |:---------------------------|:-------------| | `/close` | Close the issue or merge request | | `/reopen` | Reopen the issue or merge request | +| `/merge` | Merge (when build succeeds) | | `/title <New title>` | Change title | | `/assign @username` | Assign | | `/unassign` | Remove assignee | @@ -29,3 +30,7 @@ do. | <code>/due <in 2 days | this Friday | December 31st></code> | Set due date | | `/remove_due_date` | Remove due date | | `/wip` | Toggle the Work In Progress status | +| <code>/estimate <1w 3d 2h 14m></code> | Set time estimate | +| `/remove_estimate` | Remove estimated time | +| <code>/spend <1h 30m | -1h 5m></code> | Add or substract spent time | +| `/remove_time_spent` | Remove time spent | diff --git a/doc/workflow/README.md b/doc/workflow/README.md index 59a806de210..b317bd79ded 100644 --- a/doc/workflow/README.md +++ b/doc/workflow/README.md @@ -19,6 +19,7 @@ - [Slash commands](../user/project/slash_commands.md) - [Sharing a project with a group](share_with_group.md) - [Share projects with other groups](share_projects_with_other_groups.md) +- [Time tracking](time_tracking.md) - [Web Editor](../user/project/repository/web_editor.md) - [Releases](releases.md) - [Milestones](milestones.md) diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md index 423b095e69e..7a3628a39d7 100644 --- a/doc/workflow/importing/migrating_from_svn.md +++ b/doc/workflow/importing/migrating_from_svn.md @@ -79,7 +79,7 @@ Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the initial translation of existing SVN revisions into the Git repository: ``` -subgit install $GIT_REPOS_PATH +subgit install $GIT_REPO_PATH ``` After the initial translation is completed, the Git repository and the SVN diff --git a/doc/workflow/time-tracking/time-tracking-example.png b/doc/workflow/time-tracking/time-tracking-example.png Binary files differnew file mode 100644 index 00000000000..bbcabb602d6 --- /dev/null +++ b/doc/workflow/time-tracking/time-tracking-example.png diff --git a/doc/workflow/time-tracking/time-tracking-sidebar.png b/doc/workflow/time-tracking/time-tracking-sidebar.png Binary files differnew file mode 100644 index 00000000000..d1ff5571f95 --- /dev/null +++ b/doc/workflow/time-tracking/time-tracking-sidebar.png diff --git a/doc/workflow/time_tracking.md b/doc/workflow/time_tracking.md new file mode 100644 index 00000000000..de12994c516 --- /dev/null +++ b/doc/workflow/time_tracking.md @@ -0,0 +1,73 @@ +# Time Tracking + +> Introduced in GitLab 8.14. + +Time Tracking allows you to track estimates and time spent on issues and merge +requests within GitLab. + +## Overview + +Time Tracking lets you: +* record the time spent working on an issue or a merge request, +* add an estimate of the amount of time needed to complete an issue or a merge +request. + +You don't have to indicate an estimate to enter the time spent, and vice versa. + +Data about time tracking is shown on the issue/merge request sidebar, as shown +below. + +![Time tracking in the sidebar](time-tracking/time-tracking-sidebar.png) + +## How to enter data + +Time Tracking uses two [slash commands] that GitLab introduced with this new +feature: `/spend` and `/estimate`. + +Slash commands can be used in the body of an issue or a merge request, but also +in a comment in both an issue or a merge request. + +Below is an example of how you can use those new slash commands inside a comment. + +![Time tracking example in a comment](time-tracking/time-tracking-example.png) + +Adding time entries (time spent or estimates) is limited to project members. + +### Estimates + +To enter an estimate, write `/estimate`, followed by the time. For example, if +you need to enter an estimate of 3 days, 5 hours and 10 minutes, you would write +`/estimate 3d 5h 10m`. + +Every time you enter a new time estimate, any previous time estimates will be +overridden by this new value. There should only be one valid estimate in an +issue or a merge request. + +To remove an estimation entirely, use `/remove_estimation`. + +### Time spent + +To enter a time spent, use `/spend 3d 5h 10m`. + +Every new time spent entry will be added to the current total time spent for the +issue or the merge request. + +You can remove time by entering a negative amount: `/spend -3d` will remove 3 +days from the total time spent. You can't go below 0 minutes of time spent, +so GitLab will automatically reset the time spent if you remove a larger amount +of time compared to the time that was entered already. + +To remove all the time spent at once, use `/remove_time_spent`. + +## Configuration + +The following time units are available: +* weeks (w) +* days (d) +* hours (h) +* minutes (m) + +Default conversion rates are 1w = 5d and 1d = 8h. + +[landing]: https://about.gitlab.com/features/time-tracking +[slash-commands]: ../user/project/slash_commands.md diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb index b2bec369e0f..33a1c88e33c 100644 --- a/features/steps/dashboard/dashboard.rb +++ b/features/steps/dashboard/dashboard.rb @@ -35,7 +35,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps step 'I have group with projects' do @group = create(:group) - @project = create(:project, namespace: @group) + @project = create(:empty_project, namespace: @group) @event = create(:closed_issue_event, project: @project) @project.team << [current_user, :master] @@ -54,8 +54,8 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps end step 'group has a projects that does not belongs to me' do - @forbidden_project1 = create(:project, group: @group) - @forbidden_project2 = create(:project, group: @group) + @forbidden_project1 = create(:empty_project, group: @group) + @forbidden_project2 = create(:empty_project, group: @group) end step 'I should see 1 project at group list' do diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb index 39c65bb6cde..4e15d79ae74 100644 --- a/features/steps/dashboard/issues.rb +++ b/features/steps/dashboard/issues.rb @@ -79,13 +79,13 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps def project @project ||= begin - project = create :project + project = create(:empty_project) project.team << [current_user, :master] project end end def public_project - @public_project ||= create :project, :public + @public_project ||= create(:empty_project, :public) end end diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb index 6777101fb15..909ffec3646 100644 --- a/features/steps/dashboard/merge_requests.rb +++ b/features/steps/dashboard/merge_requests.rb @@ -105,14 +105,14 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps def project @project ||= begin - project = create :project + project = create(:project, :repository) project.team << [current_user, :master] project end end def public_project - @public_project ||= create :project, :public + @public_project ||= create(:project, :public, :repository) end def forked_project diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index c1d1eca9116..70e23098dde 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -104,7 +104,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps group = owned_group %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path| - project = create :project, path: path, group: group + project = create(:empty_project, path: path, group: group) milestone = create :milestone, title: "Version 7.2", project: project create(:label, project: project, title: 'bug') diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 0c88838767c..4dc87dc4d9c 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -109,7 +109,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps step 'Group "Owned" has archived project' do group = Group.find_by(name: 'Owned') - @archived_project = create(:project, namespace: group, archived: true, path: "archived-project") + @archived_project = create(:empty_project, :archived, namespace: group, path: "archived-project") end step 'I should see "archived" label' do diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb index ea480d2ad68..24cfbaad7fe 100644 --- a/features/steps/profile/profile.rb +++ b/features/steps/profile/profile.rb @@ -162,7 +162,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps step 'I have group with projects' do @group = create(:group) @group.add_owner(current_user) - @project = create(:project, namespace: @group) + @project = create(:project, :repository, namespace: @group) @event = create(:closed_issue_event, project: @project) @project.team << [current_user, :master] diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb index 83b9ef48392..edf78f62f9a 100644 --- a/features/steps/project/deploy_keys.rb +++ b/features/steps/project/deploy_keys.rb @@ -46,11 +46,11 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps end step 'other projects have deploy keys' do - @second_project = create(:project, namespace: create(:group)) + @second_project = create(:empty_project, namespace: create(:group)) @second_project.team << [current_user, :master] create(:deploy_keys_project, project: @second_project) - @third_project = create(:project, namespace: create(:group)) + @third_project = create(:empty_project, namespace: create(:group)) @third_project.team << [current_user, :master] create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first) end diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 70dbd030003..9a6c04fba7a 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -9,7 +9,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I am a member of project "Shop"' do - @project = create(:project, name: "Shop") + @project = create(:project, :repository, name: "Shop") @project.team << [@user, :reporter] end @@ -18,7 +18,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps end step 'I already have a project named "Shop" in my namespace' do - @my_project = create(:project, name: "Shop", namespace: current_user.namespace) + @my_project = create(:project, :repository, name: "Shop", namespace: current_user.namespace) end step 'I should see a "Name has already been taken" warning' do diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 6c14d835004..c0827ff8fc7 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -7,7 +7,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps step 'I am a member of project "Shop"' do @project = Project.find_by(name: "Shop") - @project ||= create(:project, name: "Shop") + @project ||= create(:project, :repository, name: "Shop") @project.team << [@user, :reporter] end diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb index 4fda0731e2f..0a3f4649870 100644 --- a/features/steps/project/merge_requests/acceptance.rb +++ b/features/steps/project/merge_requests/acceptance.rb @@ -28,7 +28,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps step 'There is an open Merge Request' do @user = create(:user) - @project = create(:project, :public) + @project = create(:project, :public, :repository) @project_member = create(:project_member, :developer, user: @user, project: @project) @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project) end diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb index efbc4831ce1..3cc4fe9dafb 100644 --- a/features/steps/project/merge_requests/revert.rb +++ b/features/steps/project/merge_requests/revert.rb @@ -35,7 +35,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps step 'There is an open Merge Request' do @user = create(:user) - @project = create(:project, :public) + @project = create(:project, :public, :repository) @project_member = create(:project_member, :developer, user: @user, project: @project) @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project) end diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb index 1ffd5cb9de5..92936f27c20 100644 --- a/features/steps/project/redirects.rb +++ b/features/steps/project/redirects.rb @@ -4,11 +4,11 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps include SharedProject step 'public project "Community"' do - create :project, :public, name: 'Community' + create(:empty_project, :public, name: 'Community') end step 'private project "Enterprise"' do - create :project, name: 'Enterprise' + create(:empty_project, :private, name: 'Enterprise') end step 'I visit project "Community" page' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 1cc9e37b075..f18adcadcce 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -6,7 +6,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps include RepoHelpers step "I don't have write access" do - @project = create(:project, name: "Other Project", path: "other-project") + @project = create(:project, :repository, name: "Other Project", path: "other-project") @project.team << [@user, :reporter] visit namespace_project_tree_path(@project.namespace, @project, root_ref) end diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index dee6a8a5558..9183de76881 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -8,7 +8,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I own project "Delta"' do @project = Project.find_by(name: "Delta") - @project ||= create(:project, name: "Delta", namespace: @user.namespace) + @project ||= create(:project, :repository, name: "Delta", namespace: @user.namespace) @project.team << [@user, :master] end diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb index c89f587f14d..6986c7ede56 100644 --- a/features/steps/project/team_management.rb +++ b/features/steps/project/team_management.rb @@ -137,7 +137,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps step 'I share project with group "OpenSource"' do project = Project.find_by(name: 'Shop') os_group = create(:group, name: 'OpenSource') - create(:project, group: os_group) + create(:empty_project, group: os_group) @os_user1 = create(:user) @os_user2 = create(:user) os_group.add_owner(@os_user1) diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb index fbaa408226e..ac0a1764147 100644 --- a/features/steps/shared/admin.rb +++ b/features/steps/shared/admin.rb @@ -2,7 +2,7 @@ module SharedAdmin include Spinach::DSL step 'there are projects in system' do - 2.times { create(:project) } + 2.times { create(:project, :repository) } end step 'system has users' do diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb index fe6736dacd4..de119f2c6c0 100644 --- a/features/steps/shared/group.rb +++ b/features/steps/shared/group.rb @@ -40,7 +40,7 @@ module SharedGroup user = User.find_by(name: username) || create(:user, name: username) group = Group.find_by(name: groupname) || create(:group, name: groupname) group.add_user(user, role) - project ||= create(:project, namespace: group, path: "project#{@project_count}") + project ||= create(:project, :repository, namespace: group, path: "project#{@project_count}") create(:closed_issue_event, project: project) project.team << [user, :master] @project_count += 1 diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index b51152c79c6..7a6707a7dfb 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -3,19 +3,19 @@ module SharedProject # Create a project without caring about what it's called step "I own a project" do - @project = create(:project, namespace: @user.namespace) + @project = create(:project, :repository, namespace: @user.namespace) @project.team << [@user, :master] end step "project exists in some group namespace" do @group = create(:group, name: 'some group') - @project = create(:project, namespace: @group, public_builds: false) + @project = create(:project, :repository, namespace: @group, public_builds: false) end # Create a specific project called "Shop" step 'I own project "Shop"' do @project = Project.find_by(name: "Shop") - @project ||= create(:project, name: "Shop", namespace: @user.namespace) + @project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace) @project.team << [@user, :master] end @@ -40,7 +40,7 @@ module SharedProject # Create another specific project called "Forum" step 'I own project "Forum"' do @project = Project.find_by(name: "Forum") - @project ||= create(:project, name: "Forum", namespace: @user.namespace, path: 'forum_project') + @project ||= create(:project, :repository, name: "Forum", namespace: @user.namespace, path: 'forum_project') @project.build_project_feature @project.project_feature.save @project.team << [@user, :master] @@ -97,7 +97,7 @@ module SharedProject step 'I should see project settings' do expect(current_path).to eq edit_namespace_project_path(@project.namespace, @project) expect(page).to have_content("Project name") - expect(page).to have_content("Feature Visibility") + expect(page).to have_content("Sharing & Permissions") end def current_project @@ -121,7 +121,7 @@ module SharedProject # ---------------------------------------- step 'archived project "Archive"' do - create :project, :public, archived: true, name: 'Archive' + create(:project, :archived, :public, :repository, name: 'Archive') end step 'I should not see project "Archive"' do @@ -144,7 +144,7 @@ module SharedProject # ---------------------------------------- step 'private project "Enterprise"' do - create :project, name: 'Enterprise' + create(:project, :private, :repository, name: 'Enterprise') end step 'I should see project "Enterprise"' do @@ -156,7 +156,7 @@ module SharedProject end step 'internal project "Internal"' do - create :project, :internal, name: 'Internal' + create(:project, :internal, :repository, name: 'Internal') end step 'I should see project "Internal"' do @@ -168,7 +168,7 @@ module SharedProject end step 'public project "Community"' do - create :project, :public, name: 'Community' + create(:project, :public, :repository, name: 'Community') end step 'I should see project "Community"' do diff --git a/features/steps/user.rb b/features/steps/user.rb index 59385a6ab59..271c9b097d4 100644 --- a/features/steps/user.rb +++ b/features/steps/user.rb @@ -38,6 +38,6 @@ class Spinach::Features::User < Spinach::FeatureSteps end def contributed_project - @contributed_project ||= create(:project, :public) + @contributed_project ||= create(:empty_project, :public) end end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 4bbdf06a49c..b6e6820c3f4 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -78,6 +78,8 @@ module API description: params[:description] ) + render_validation_error!(status) if status.invalid? + begin case params[:state] when 'pending' diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 885ce7d44bc..9f59939e9ae 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -268,6 +268,13 @@ module API end end + class IssuableTimeStats < Grape::Entity + expose :time_estimate + expose :total_time_spent + expose :human_time_estimate + expose :human_total_time_spent + end + class ExternalIssue < Grape::Entity expose :title expose :id diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index eb2d370c68e..6b81fbf294e 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -86,6 +86,10 @@ module API IssuesFinder.new(current_user, project_id: user_project.id).find(id) end + def find_project_merge_request(id) + MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id) + end + def authenticate! unauthorized! unless current_user end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 161269cbd41..fe016c1ec0a 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -89,6 +89,8 @@ module API requires :id, type: String, desc: 'The ID of a project' end resource :projects do + include TimeTrackingEndpoints + desc 'Get a list of project issues' do success Entities::Issue end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 5d1fe22f2df..e77af4b7a0d 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -10,6 +10,8 @@ module API requires :id, type: String, desc: 'The ID of a project' end resource :projects do + include TimeTrackingEndpoints + helpers do def handle_merge_request_errors!(errors) if errors[:project_access].any? @@ -96,7 +98,7 @@ module API requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' end delete ":id/merge_requests/:merge_request_id" do - merge_request = user_project.merge_requests.find_by(id: params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize!(:destroy_merge_request, merge_request) merge_request.destroy @@ -116,7 +118,7 @@ module API success Entities::MergeRequest end get path do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize! :read_merge_request, merge_request present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project end @@ -125,7 +127,7 @@ module API success Entities::RepoCommit end get "#{path}/commits" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize! :read_merge_request, merge_request present merge_request.commits, with: Entities::RepoCommit end @@ -134,7 +136,7 @@ module API success Entities::MergeRequestChanges end get "#{path}/changes" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize! :read_merge_request, merge_request present merge_request, with: Entities::MergeRequestChanges, current_user: current_user end @@ -153,7 +155,7 @@ module API :remove_source_branch end put path do - merge_request = user_project.merge_requests.find(params.delete(:merge_request_id)) + merge_request = find_project_merge_request(params.delete(:merge_request_id)) authorize! :update_merge_request, merge_request mr_params = declared_params(include_missing: false) @@ -180,7 +182,7 @@ module API optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' end put "#{path}/merge" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) # Merge request can not be merged # because user dont have permissions to push into target branch @@ -216,7 +218,7 @@ module API success Entities::MergeRequest end post "#{path}/cancel_merge_when_build_succeeds" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) @@ -233,7 +235,7 @@ module API use :pagination end get "#{path}/comments" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize! :read_merge_request, merge_request @@ -248,7 +250,7 @@ module API requires :note, type: String, desc: 'The text of the comment' end post "#{path}/comments" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) authorize! :create_note, merge_request opts = { @@ -273,7 +275,7 @@ module API use :pagination end get "#{path}/closes_issues" do - merge_request = user_project.merge_requests.find(params[:merge_request_id]) + merge_request = find_project_merge_request(params[:merge_request_id]) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) present paginate(issues), with: issue_entity(user_project), current_user: current_user end diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb new file mode 100644 index 00000000000..85b5f7d98b8 --- /dev/null +++ b/lib/api/time_tracking_endpoints.rb @@ -0,0 +1,114 @@ +module API + module TimeTrackingEndpoints + extend ActiveSupport::Concern + + included do + helpers do + def issuable_name + declared_params.has_key?(:issue_id) ? 'issue' : 'merge_request' + end + + def issuable_key + "#{issuable_name}_id".to_sym + end + + def update_issuable_key + "update_#{issuable_name}".to_sym + end + + def read_issuable_key + "read_#{issuable_name}".to_sym + end + + def load_issuable + @issuable ||= begin + case issuable_name + when 'issue' + find_project_issue(params.delete(issuable_key)) + when 'merge_request' + find_project_merge_request(params.delete(issuable_key)) + end + end + end + + def update_issuable(attrs) + custom_params = declared_params(include_missing: false) + custom_params.merge!(attrs) + + issuable = update_service.new(user_project, current_user, custom_params).execute(load_issuable) + if issuable.valid? + present issuable, with: Entities::IssuableTimeStats + else + render_validation_error!(issuable) + end + end + + def update_service + issuable_name == 'issue' ? ::Issues::UpdateService : ::MergeRequests::UpdateService + end + end + + issuable_name = name.end_with?('Issues') ? 'issue' : 'merge_request' + issuable_collection_name = issuable_name.pluralize + issuable_key = "#{issuable_name}_id".to_sym + + desc "Set a time estimate for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + requires :duration, type: String, desc: 'The duration to be parsed' + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/time_estimate" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(time_estimate: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration))) + end + + desc "Reset the time estimate for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_time_estimate" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(time_estimate: 0) + end + + desc "Add spent time for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + requires :duration, type: String, desc: 'The duration to be parsed' + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/add_spent_time" do + authorize! update_issuable_key, load_issuable + + update_issuable(spend_time: { + duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)), + user: current_user + }) + end + + desc "Reset spent time for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + post ":id/#{issuable_collection_name}/:#{issuable_key}/reset_spent_time" do + authorize! update_issuable_key, load_issuable + + status :ok + update_issuable(spend_time: { duration: :reset, user: current_user }) + end + + desc "Show time stats for a project #{issuable_name}" + params do + requires issuable_key, type: Integer, desc: "The ID of a project #{issuable_name}" + end + get ":id/#{issuable_collection_name}/:#{issuable_key}/time_stats" do + authorize! read_issuable_key, load_issuable + + present load_issuable, with: Entities::IssuableTimeStats + end + end + end +end diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index 84bfeac8041..ab7af1cad21 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -20,10 +20,10 @@ module Banzai # Examples: # # data_attribute(project: 1, issue: 2) - # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" + # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\"" # # data_attribute(project: 3, merge_request: 4) - # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" + # # => "data-reference-type=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\"" # # Returns a String def data_attribute(attributes = {}) @@ -31,7 +31,9 @@ module Banzai attributes[:reference_type] ||= self.class.reference_type attributes.delete(:original) if context[:no_original_data] - attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ") + attributes.map do |key, value| + %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") + end.join(' ') end def escape_once(html) diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb new file mode 100644 index 00000000000..4969a350862 --- /dev/null +++ b/lib/gitlab/ci/status/external/common.rb @@ -0,0 +1,22 @@ +module Gitlab + module Ci + module Status + module External + module Common + def has_details? + subject.target_url.present? && + can?(user, :read_commit_status, subject) + end + + def details_path + subject.target_url + end + + def has_action? + false + end + end + end + end + end +end diff --git a/lib/gitlab/ci/status/external/factory.rb b/lib/gitlab/ci/status/external/factory.rb new file mode 100644 index 00000000000..07b15bd8d97 --- /dev/null +++ b/lib/gitlab/ci/status/external/factory.rb @@ -0,0 +1,13 @@ +module Gitlab + module Ci + module Status + module External + class Factory < Status::Factory + def self.common_helpers + Status::External::Common + end + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb index 53a148ad703..0d8791d396b 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb @@ -1,13 +1,13 @@ module Gitlab module CycleAnalytics - class BaseEvent - include MetricsTables + class BaseEventFetcher + include BaseQuery - attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query + attr_reader :projections, :query, :stage, :order - def initialize(project:, options:) - @query = EventsQuery.new(project: project, options: options) + def initialize(project:, stage:, options:) @project = project + @stage = stage @options = options end @@ -19,10 +19,8 @@ module Gitlab end.compact end - def custom_query(_base_query); end - def order - @order || @start_time_attrs + @order || default_order end private @@ -34,7 +32,17 @@ module Gitlab end def event_result - @event_result ||= @query.execute(self).to_a + @event_result ||= ActiveRecord::Base.connection.exec_query(events_query.to_sql).to_a + end + + def events_query + diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs]) + + base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc) + end + + def default_order + [@options[:start_time_attrs]].flatten.first end def serialize(_event) diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb new file mode 100644 index 00000000000..d560dca45c8 --- /dev/null +++ b/lib/gitlab/cycle_analytics/base_query.rb @@ -0,0 +1,31 @@ +module Gitlab + module CycleAnalytics + module BaseQuery + include MetricsTables + include Gitlab::Database::Median + include Gitlab::Database::DateTime + + private + + def base_query + @base_query ||= stage_query + end + + def stage_query + query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])). + join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])). + where(issue_table[:project_id].eq(@project.id)). + where(issue_table[:deleted_at].eq(nil)). + where(issue_table[:created_at].gteq(@options[:from])) + + # Load merge_requests + query = query.join(mr_table, Arel::Nodes::OuterJoin). + on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])). + join(mr_metrics_table). + on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) + + query + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb new file mode 100644 index 00000000000..74bbcdcb3dd --- /dev/null +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -0,0 +1,54 @@ +module Gitlab + module CycleAnalytics + class BaseStage + include BaseQuery + + def initialize(project:, options:) + @project = project + @options = options + end + + def events + event_fetcher.fetch + end + + def as_json + AnalyticsStageSerializer.new.represent(self).as_json + end + + def title + name.to_s.capitalize + end + + def median + cte_table = Arel::Table.new("cte_table_for_#{name}") + + # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). + # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). + # We compute the (end_time - start_time) interval, and give it an alias based on the current + # cycle analytics stage. + interval_query = Arel::Nodes::As.new( + cte_table, + subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s)) + + median_datetime(cte_table, interval_query, name) + end + + def name + raise NotImplementedError.new("Expected #{self.name} to implement name") + end + + private + + def event_fetcher + @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project, + stage: name, + options: event_options) + end + + def event_options + @options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb index b394a02cc52..5245b9ca8fc 100644 --- a/lib/gitlab/cycle_analytics/review_event.rb +++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb @@ -1,22 +1,22 @@ module Gitlab module CycleAnalytics - class ReviewEvent < BaseEvent + class CodeEventFetcher < BaseEventFetcher include MergeRequestAllowed def initialize(*args) - @stage = :review - @start_time_attrs = mr_table[:created_at] - @end_time_attrs = mr_metrics_table[:merged_at] @projections = [mr_table[:title], mr_table[:iid], mr_table[:id], mr_table[:created_at], mr_table[:state], mr_table[:author_id]] + @order = mr_table[:created_at] super(*args) end + private + def serialize(event) AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json end diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb new file mode 100644 index 00000000000..d1bc2055ba8 --- /dev/null +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -0,0 +1,21 @@ +module Gitlab + module CycleAnalytics + class CodeStage < BaseStage + def start_time_attrs + @start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_table[:created_at] + end + + def name + :code + end + + def description + "Time until first merge request" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/event_fetcher.rb b/lib/gitlab/cycle_analytics/event_fetcher.rb new file mode 100644 index 00000000000..50e126cf00b --- /dev/null +++ b/lib/gitlab/cycle_analytics/event_fetcher.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module EventFetcher + def self.[](stage_name) + CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher") + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/events.rb b/lib/gitlab/cycle_analytics/events.rb deleted file mode 100644 index 2d703d76cbb..00000000000 --- a/lib/gitlab/cycle_analytics/events.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Gitlab - module CycleAnalytics - class Events - def initialize(project:, options:) - @project = project - @options = options - end - - def issue_events - IssueEvent.new(project: @project, options: @options).fetch - end - - def plan_events - PlanEvent.new(project: @project, options: @options).fetch - end - - def code_events - CodeEvent.new(project: @project, options: @options).fetch - end - - def test_events - TestEvent.new(project: @project, options: @options).fetch - end - - def review_events - ReviewEvent.new(project: @project, options: @options).fetch - end - - def staging_events - StagingEvent.new(project: @project, options: @options).fetch - end - - def production_events - ProductionEvent.new(project: @project, options: @options).fetch - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb deleted file mode 100644 index 2418832ccc2..00000000000 --- a/lib/gitlab/cycle_analytics/events_query.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Gitlab - module CycleAnalytics - class EventsQuery - attr_reader :project - - def initialize(project:, options: {}) - @project = project - @from = options[:from] - @branch = options[:branch] - @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: @from, branch: @branch) - end - - def execute(stage_class) - @stage_class = stage_class - - ActiveRecord::Base.connection.exec_query(query.to_sql) - end - - private - - def query - base_query = @fetcher.base_query_for(@stage_class.stage) - diff_fn = @fetcher.subtract_datetimes_diff(base_query, @stage_class.start_time_attrs, @stage_class.end_time_attrs) - - @stage_class.custom_query(base_query) - - base_query.project(extract_epoch(diff_fn).as('total_time'), *@stage_class.projections).order(@stage_class.order.desc) - end - - def extract_epoch(arel_attribute) - return arel_attribute unless Gitlab::Database.postgresql? - - Arel.sql(%Q{EXTRACT(EPOCH FROM (#{arel_attribute.to_sql}))}) - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb deleted file mode 100644 index 705b7e5ce24..00000000000 --- a/lib/gitlab/cycle_analytics/issue_event.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Gitlab - module CycleAnalytics - class IssueEvent < BaseEvent - include IssueAllowed - - def initialize(*args) - @stage = :issue - @start_time_attrs = issue_table[:created_at] - @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], - issue_metrics_table[:first_added_to_board_at]] - @projections = [issue_table[:title], - issue_table[:iid], - issue_table[:id], - issue_table[:created_at], - issue_table[:author_id]] - - super(*args) - end - - private - - def serialize(event) - AnalyticsIssueSerializer.new(project: @project).represent(event).as_json - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb index 4868c3c6237..0d8da99455e 100644 --- a/lib/gitlab/cycle_analytics/production_event.rb +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -1,12 +1,9 @@ module Gitlab module CycleAnalytics - class ProductionEvent < BaseEvent + class IssueEventFetcher < BaseEventFetcher include IssueAllowed def initialize(*args) - @stage = :production - @start_time_attrs = issue_table[:created_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] @projections = [issue_table[:title], issue_table[:iid], issue_table[:id], diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb new file mode 100644 index 00000000000..d2068fbc38f --- /dev/null +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class IssueStage < BaseStage + def start_time_attrs + @start_time_attrs ||= issue_table[:created_at] + end + + def end_time_attrs + @end_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] + end + + def name + :issue + end + + def description + "Time before an issue gets scheduled" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb deleted file mode 100644 index b71e8735e27..00000000000 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ /dev/null @@ -1,60 +0,0 @@ -module Gitlab - module CycleAnalytics - class MetricsFetcher - include Gitlab::Database::Median - include Gitlab::Database::DateTime - include MetricsTables - - DEPLOYMENT_METRIC_STAGES = %i[production staging] - - def initialize(project:, from:, branch:) - @project = project - @project = project - @from = from - @branch = branch - end - - def calculate_metric(name, start_time_attrs, end_time_attrs) - cte_table = Arel::Table.new("cte_table_for_#{name}") - - # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). - # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). - # We compute the (end_time - start_time) interval, and give it an alias based on the current - # cycle analytics stage. - interval_query = Arel::Nodes::As.new( - cte_table, - subtract_datetimes(base_query_for(name), start_time_attrs, end_time_attrs, name.to_s)) - - median_datetime(cte_table, interval_query, name) - end - - # Join table with a row for every <issue,merge_request> pair (where the merge request - # closes the given issue) with issue and merge request metrics included. The metrics - # are loaded with an inner join, so issues / merge requests without metrics are - # automatically excluded. - def base_query_for(name) - # Load issues - query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])). - join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])). - where(issue_table[:project_id].eq(@project.id)). - where(issue_table[:deleted_at].eq(nil)). - where(issue_table[:created_at].gteq(@from)) - - query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch - - # Load merge_requests - query = query.join(mr_table, Arel::Nodes::OuterJoin). - on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])). - join(mr_metrics_table). - on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) - - if DEPLOYMENT_METRIC_STAGES.include?(name) - # Limit to merge requests that have been deployed to production after `@from` - query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from)) - end - - query - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb index 7c3f0e9989f..88a8710dbe6 100644 --- a/lib/gitlab/cycle_analytics/plan_event.rb +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -1,19 +1,17 @@ module Gitlab module CycleAnalytics - class PlanEvent < BaseEvent + class PlanEventFetcher < BaseEventFetcher def initialize(*args) - @stage = :plan - @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at] - @end_time_attrs = [issue_metrics_table[:first_added_to_board_at], - issue_metrics_table[:first_mentioned_in_commit_at]] @projections = [mr_diff_table[:st_commits].as('commits'), issue_metrics_table[:first_mentioned_in_commit_at]] super(*args) end - def custom_query(base_query) + def events_query base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id])) + + super end private diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb new file mode 100644 index 00000000000..3b4dfc6a30e --- /dev/null +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class PlanStage < BaseStage + def start_time_attrs + @start_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] + end + + def end_time_attrs + @end_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at] + end + + def name + :plan + end + + def description + "Time before an issue starts implementation" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb new file mode 100644 index 00000000000..0fa2e87f673 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb @@ -0,0 +1,6 @@ +module Gitlab + module CycleAnalytics + class ProductionEventFetcher < IssueEventFetcher + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb new file mode 100644 index 00000000000..d693443bfa4 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_helper.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module ProductionHelper + def stage_query + super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from])) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb new file mode 100644 index 00000000000..2a6bcc80116 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -0,0 +1,28 @@ +module Gitlab + module CycleAnalytics + class ProductionStage < BaseStage + include ProductionHelper + + def start_time_attrs + @start_time_attrs ||= issue_table[:created_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at] + end + + def name + :production + end + + def description + "From issue creation until deploy to production" + end + + def query + # Limit to merge requests that have been deployed to production after `@from` + query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from)) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb index 2afdf0b8518..4df0bd06393 100644 --- a/lib/gitlab/cycle_analytics/code_event.rb +++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb @@ -1,25 +1,19 @@ module Gitlab module CycleAnalytics - class CodeEvent < BaseEvent + class ReviewEventFetcher < BaseEventFetcher include MergeRequestAllowed def initialize(*args) - @stage = :code - @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] - @end_time_attrs = mr_table[:created_at] @projections = [mr_table[:title], mr_table[:iid], mr_table[:id], mr_table[:created_at], mr_table[:state], mr_table[:author_id]] - @order = mr_table[:created_at] super(*args) end - private - def serialize(event) AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb new file mode 100644 index 00000000000..fbaa3010d81 --- /dev/null +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -0,0 +1,21 @@ +module Gitlab + module CycleAnalytics + class ReviewStage < BaseStage + def start_time_attrs + @start_time_attrs ||= mr_table[:created_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:merged_at] + end + + def name + :review + end + + def description + "Time between merge request creation and merge/close" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/stage.rb b/lib/gitlab/cycle_analytics/stage.rb new file mode 100644 index 00000000000..28e0455df59 --- /dev/null +++ b/lib/gitlab/cycle_analytics/stage.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module Stage + def self.[](stage_name) + CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage") + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb new file mode 100644 index 00000000000..b34baf5b081 --- /dev/null +++ b/lib/gitlab/cycle_analytics/stage_summary.rb @@ -0,0 +1,23 @@ +module Gitlab + module CycleAnalytics + class StageSummary + def initialize(project, from:, current_user:) + @project = project + @from = from + @current_user = current_user + end + + def data + [serialize(Summary::Issue.new(project: @project, from: @from, current_user: @current_user)), + serialize(Summary::Commit.new(project: @project, from: @from)), + serialize(Summary::Deploy.new(project: @project, from: @from))] + end + + private + + def serialize(summary_object) + AnalyticsSummarySerializer.new.represent(summary_object).as_json + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb index a1f30b716f6..a34731a5fcd 100644 --- a/lib/gitlab/cycle_analytics/staging_event.rb +++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb @@ -1,10 +1,7 @@ module Gitlab module CycleAnalytics - class StagingEvent < BaseEvent + class StagingEventFetcher < BaseEventFetcher def initialize(*args) - @stage = :staging - @start_time_attrs = mr_metrics_table[:merged_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] @projections = [build_table[:id]] @order = build_table[:created_at] @@ -17,8 +14,10 @@ module Gitlab super end - def custom_query(base_query) + def events_query base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) + + super end private diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb new file mode 100644 index 00000000000..945909a4d62 --- /dev/null +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class StagingStage < BaseStage + include ProductionHelper + def start_time_attrs + @start_time_attrs ||= mr_metrics_table[:merged_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at] + end + + def name + :staging + end + + def description + "From merge request merge until deploy to production" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb new file mode 100644 index 00000000000..43fa3795e5c --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/base.rb @@ -0,0 +1,20 @@ +module Gitlab + module CycleAnalytics + module Summary + class Base + def initialize(project:, from:) + @project = project + @from = from + end + + def title + self.class.name.demodulize + end + + def value + raise NotImplementedError.new("Expected #{self.name} to implement value") + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb new file mode 100644 index 00000000000..7b8faa4d854 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/commit.rb @@ -0,0 +1,39 @@ +module Gitlab + module CycleAnalytics + module Summary + class Commit < Base + def value + @value ||= count_commits + 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 + 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 + + output, status = Gitlab::Popen.popen(cmd) + + raise IOError, output unless status.zero? + + output.lines.count + end + + def ref + @ref ||= @project.default_branch.presence + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb new file mode 100644 index 00000000000..06032e9200e --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/deploy.rb @@ -0,0 +1,11 @@ +module Gitlab + module CycleAnalytics + module Summary + class Deploy < Base + def value + @value ||= @project.deployments.where("created_at > ?", @from).count + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb new file mode 100644 index 00000000000..008468f24b9 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/issue.rb @@ -0,0 +1,21 @@ +module Gitlab + module CycleAnalytics + module Summary + class Issue < Base + def initialize(project:, from:, current_user:) + @project = project + @from = from + @current_user = current_user + end + + def title + 'New Issue' + end + + def value + @value ||= IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb deleted file mode 100644 index d553d0b5aec..00000000000 --- a/lib/gitlab/cycle_analytics/test_event.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Gitlab - module CycleAnalytics - class TestEvent < StagingEvent - def initialize(*args) - super(*args) - - @stage = :test - @start_time_attrs = mr_metrics_table[:latest_build_started_at] - @end_time_attrs = mr_metrics_table[:latest_build_finished_at] - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/test_event_fetcher.rb b/lib/gitlab/cycle_analytics/test_event_fetcher.rb new file mode 100644 index 00000000000..a2589c6601a --- /dev/null +++ b/lib/gitlab/cycle_analytics/test_event_fetcher.rb @@ -0,0 +1,6 @@ +module Gitlab + module CycleAnalytics + class TestEventFetcher < StagingEventFetcher + end + end +end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb new file mode 100644 index 00000000000..0079d56e0e4 --- /dev/null +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -0,0 +1,29 @@ +module Gitlab + module CycleAnalytics + class TestStage < BaseStage + def start_time_attrs + @start_time_attrs ||= mr_metrics_table[:latest_build_started_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:latest_build_finished_at] + end + + def name + :test + end + + def description + "Total test time for all commits/merges" + end + + def stage_query + if @options[:branch] + super.where(build_table[:ref].eq(@options[:branch])) + else + super + end + end + end + end +end diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb index 1444d25ebc7..08607c27c09 100644 --- a/lib/gitlab/database/median.rb +++ b/lib/gitlab/database/median.rb @@ -103,6 +103,11 @@ module Gitlab Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")}) end + def extract_diff_epoch(diff) + return diff unless Gitlab::Database.postgresql? + + Arel.sql(%Q{EXTRACT(EPOCH FROM (#{diff.to_sql}))}) + end # Need to cast '0' to an INTERVAL before we can check if the interval is positive def zero_interval Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")]) diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb index 2913230e979..58193391926 100644 --- a/lib/gitlab/git/blame.rb +++ b/lib/gitlab/git/blame.rb @@ -1,5 +1,3 @@ -require_relative 'encoding_helper' - module Gitlab module Git class Blame diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index 4a623311c14..b742d9e1e4b 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -1,6 +1,3 @@ -require_relative 'encoding_helper' -require_relative 'path_helper' - module Gitlab module Git class Blob diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 79b23d59b3a..7068e68a855 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -1,6 +1,4 @@ # Gitlab::Git::Repository is a wrapper around native Rugged::Repository object -require_relative 'encoding_helper' -require_relative 'path_helper' require 'forwardable' require 'tempfile' require 'forwardable' diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index e6ecd118609..08ad3274b38 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -6,6 +6,7 @@ project_tree: - :events - issues: - :events + - :timelogs - notes: - :author - :events @@ -27,6 +28,7 @@ project_tree: - :events - :merge_request_diff - :events + - :timelogs - label_links: - label: :priorities diff --git a/lib/gitlab/time_tracking_formatter.rb b/lib/gitlab/time_tracking_formatter.rb new file mode 100644 index 00000000000..d615c24149a --- /dev/null +++ b/lib/gitlab/time_tracking_formatter.rb @@ -0,0 +1,34 @@ +module Gitlab + module TimeTrackingFormatter + extend self + + def parse(string) + with_custom_config do + string.sub!(/\A-/, '') + + seconds = ChronicDuration.parse(string, default_unit: 'hours') rescue nil + seconds *= -1 if seconds && Regexp.last_match + seconds + end + end + + def output(seconds) + with_custom_config do + ChronicDuration.output(seconds, format: :short, limit_to_hours: false, weeks: true) rescue nil + end + end + + def with_custom_config + # We may want to configure it through project settings in a future version. + ChronicDuration.hours_per_day = 8 + ChronicDuration.days_per_week = 5 + + result = yield + + ChronicDuration.hours_per_day = 24 + ChronicDuration.days_per_week = 7 + + result + end + end +end diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb new file mode 100644 index 00000000000..83c8ba5c1cf --- /dev/null +++ b/lib/gitlab/view/presenter/base.rb @@ -0,0 +1,28 @@ +module Gitlab + module View + module Presenter + module Base + extend ActiveSupport::Concern + + include Gitlab::Routing + include Gitlab::Allowable + + attr_reader :subject + + def can?(user, action, overriden_subject = nil) + super(user, action, overriden_subject || subject) + end + + class_methods do + def presenter? + true + end + + def presents(name) + define_method(name) { subject } + end + end + end + end + end +end diff --git a/lib/gitlab/view/presenter/delegated.rb b/lib/gitlab/view/presenter/delegated.rb new file mode 100644 index 00000000000..f4d330c590e --- /dev/null +++ b/lib/gitlab/view/presenter/delegated.rb @@ -0,0 +1,19 @@ +module Gitlab + module View + module Presenter + class Delegated < SimpleDelegator + include Gitlab::View::Presenter::Base + + def initialize(subject, **attributes) + @subject = subject + + attributes.each do |key, value| + define_singleton_method(key) { value } + end + + super(subject) + end + end + end + end +end diff --git a/lib/gitlab/view/presenter/factory.rb b/lib/gitlab/view/presenter/factory.rb new file mode 100644 index 00000000000..d172d61e2c9 --- /dev/null +++ b/lib/gitlab/view/presenter/factory.rb @@ -0,0 +1,24 @@ +module Gitlab + module View + module Presenter + class Factory + def initialize(subject, **attributes) + @subject = subject + @attributes = attributes + end + + def fabricate! + presenter_class.new(subject, attributes) + end + + private + + attr_reader :subject, :attributes + + def presenter_class + "#{subject.class.name}Presenter".constantize + end + end + end + end +end diff --git a/lib/gitlab/view/presenter/simple.rb b/lib/gitlab/view/presenter/simple.rb new file mode 100644 index 00000000000..b7653a0f3cc --- /dev/null +++ b/lib/gitlab/view/presenter/simple.rb @@ -0,0 +1,17 @@ +module Gitlab + module View + module Presenter + class Simple + include Gitlab::View::Presenter::Base + + def initialize(subject, **attributes) + @subject = subject + + attributes.each do |key, value| + define_singleton_method(key) { value } + end + end + end + end + end +end diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index 6f27972c4e4..5e94fba97bf 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -7,9 +7,4 @@ namespace :dev do Rake::Task["gitlab:setup"].invoke Rake::Task["gitlab:shell:setup"].invoke end - - desc 'GitLab | Start/restart foreman and watch for changes' - task :foreman => :environment do - sh 'rerun --dir app,config,lib -- foreman start' - end end diff --git a/public/robots.txt b/public/robots.txt index 7d69fad59d1..123272a9834 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -4,13 +4,12 @@ # User-Agent: * # Disallow: / -User-Agent: * - # Add a 1 second delay between successive requests to the same server, limits resources used by crawler # Only some crawlers respect this setting, e.g. Googlebot does not # Crawl-delay: 1 # Based on details in https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/routes.rb, https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/routing, and using application +User-Agent: * Disallow: /autocomplete/users Disallow: /search Disallow: /api @@ -23,12 +22,14 @@ Disallow: /groups/*/edit Disallow: /users # Global snippets +User-Agent: * Disallow: /s/ Disallow: /snippets/new Disallow: /snippets/*/edit Disallow: /snippets/*/raw # Project details +User-Agent: * Disallow: /*/*.git Disallow: /*/*/fork/new Disallow: /*/*/repository/archive* diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb new file mode 100644 index 00000000000..e5cdd52307e --- /dev/null +++ b/spec/controllers/admin/services_controller_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Admin::ServicesController do + let(:admin) { create(:admin) } + + before { sign_in(admin) } + + describe 'GET #edit' do + let!(:project) { create(:empty_project) } + + Service.available_services_names.each do |service_name| + context "#{service_name}" do + let!(:service) do + service_template = service_name.concat("_service").camelize.constantize + service_template.where(template: true).first_or_create + end + + it 'successfully displays the template' do + get :edit, id: service.id + + expect(response).to have_http_status(200) + end + end + end + end +end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 646b097d74e..0fa06a38d2a 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -1,9 +1,10 @@ require 'spec_helper' describe Projects::CommitController do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:commit) { project.commit("master") } + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:commit) { project.commit("master") } + let(:pipeline) { create(:ci_pipeline, project: project, commit: commit) } let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } let(:master_pickable_commit) { project.commit(master_pickable_sha) } @@ -309,4 +310,35 @@ describe Projects::CommitController do end end end + + describe 'GET pipelines' do + def get_pipelines(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param + } + + get :pipelines, params.merge(extra_params) + end + + context 'when the commit exists' do + context 'when the commit has one or more pipelines' do + it 'shows pipelines' do + get_pipelines(id: commit.id) + + expect(response).to be_ok + end + end + end + + context 'when the commit does not exist' do + before do + get_pipelines(id: 'e7a412c8da9f6d0081a633a4a402dde1c4694ebd') + end + + it 'returns a 404' do + expect(response).to have_http_status(404) + end + end + end end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 7a57801c437..b03c4b52de6 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -64,6 +64,36 @@ describe Projects::CompareController do expect(assigns(:diffs)).to eq(nil) expect(assigns(:commits)).to eq(nil) end + + it 'redirects back to index when params[:from] is empty and preserves params[:to]' do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: '', + to: 'master') + + expect(response).to redirect_to(namespace_project_compare_index_path(project.namespace, project, to: 'master')) + end + + it 'redirects back to index when params[:to] is empty and preserves params[:from]' do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: 'master', + to: '') + + expect(response).to redirect_to(namespace_project_compare_index_path(project.namespace, project, from: 'master')) + end + + it 'redirects back to index when params[:from] and params[:to] are empty' do + post(:create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + from: '', + to: '') + + expect(response).to redirect_to(namespace_project_compare_index_path) + end end describe 'GET diff_for_path' do diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index e2321f2034b..b5987a83df0 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -326,6 +326,20 @@ describe Projects::IssuesController do end describe 'POST #create' do + def post_new_issue(attrs = {}) + sign_in(user) + project = create(:empty_project, :public) + project.team << [user, :developer] + + post :create, { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + issue: { title: 'Title', description: 'Description' }.merge(attrs) + } + + project.issues.first + end + context 'resolving discussions in MergeRequest' do let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } let(:merge_request) { discussion.noteable } @@ -369,13 +383,7 @@ describe Projects::IssuesController do end def post_spam_issue - sign_in(user) - spam_project = create(:empty_project, :public) - post :create, { - namespace_id: spam_project.namespace.to_param, - project_id: spam_project.to_param, - issue: { title: 'Spam Title', description: 'Spam lives here' } - } + post_new_issue(title: 'Spam Title', description: 'Spam lives here') end it 'rejects an issue recognized as spam' do @@ -396,18 +404,26 @@ describe Projects::IssuesController do request.env['action_dispatch.remote_ip'] = '127.0.0.1' end - def post_new_issue + it 'creates a user agent detail' do + expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1) + end + end + + context 'when description has slash commands' do + before do sign_in(user) - project = create(:empty_project, :public) - post :create, { - namespace_id: project.namespace.to_param, - project_id: project.to_param, - issue: { title: 'Title', description: 'Description' } - } end - it 'creates a user agent detail' do - expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1) + it 'can add spent time' do + issue = post_new_issue(description: '/spend 1h') + + expect(issue.total_time_spent).to eq(3600) + end + + it 'can set the time estimate' do + issue = post_new_issue(description: '/estimate 2h') + + expect(issue.time_estimate).to eq(7200) end end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 2a411d78395..7ea3ea4f376 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1048,4 +1048,72 @@ describe Projects::MergeRequestsController do end end end + + describe 'GET merge_widget_refresh' do + let(:params) do + { + namespace_id: project.namespace.path, + project_id: project.path, + id: merge_request.iid, + format: :raw + } + end + + before do + project.team << [user, :developer] + xhr :get, :merge_widget_refresh, params + end + + context 'when merge in progress' do + let(:merge_request) { create(:merge_request, source_project: project, in_progress_merge_commit_sha: 'sha') } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'sets status to :success' do + expect(assigns(:status)).to eq(:success) + expect(response).to render_template('merge') + end + end + + context 'when merge request was merged already' do + let(:merge_request) { create(:merge_request, source_project: project, state: :merged) } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'sets status to :success' do + expect(assigns(:status)).to eq(:success) + expect(response).to render_template('merge') + end + end + + context 'when waiting for build' do + let(:merge_request) { create(:merge_request, source_project: project, merge_when_build_succeeds: true, merge_user: user) } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'sets status to :merge_when_build_succeeds' do + expect(assigns(:status)).to eq(:merge_when_build_succeeds) + expect(response).to render_template('merge') + end + end + + context 'when no special status for MR' do + let(:merge_request) { create(:merge_request, source_project: project) } + + it 'returns an OK response' do + expect(response).to have_http_status(:ok) + end + + it 'sets status to nil' do + expect(assigns(:status)).to be_nil + expect(response).to render_template('merge') + end + end + end end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 92e38b02615..9f6d4ec6537 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -14,6 +14,54 @@ describe Projects::NotesController do } end + describe 'POST create' do + let(:merge_request) { create(:merge_request) } + let(:request_params) do + { + note: { note: 'some note', noteable_id: merge_request.id, noteable_type: 'MergeRequest' }, + namespace_id: project.namespace, + project_id: project, + merge_request_diff_head_sha: 'sha' + } + end + + before do + sign_in(user) + project.team << [user, :developer] + end + + it "returns status 302 for html" do + post :create, request_params + + expect(response).to have_http_status(302) + end + + it "returns status 200 for json" do + post :create, request_params.merge(format: :json) + + expect(response).to have_http_status(200) + end + + context 'when merge_request_diff_head_sha present' do + before do + service_params = { + note: 'some note', + noteable_id: merge_request.id.to_s, + noteable_type: 'MergeRequest', + merge_request_diff_head_sha: 'sha' + } + + expect(Notes::CreateService).to receive(:new).with(project, user, service_params).and_return(double(execute: true)) + end + + it "returns status 302 for html" do + post :create, request_params + + expect(response).to have_http_status(302) + end + end + end + describe 'POST toggle_award_emoji' do before do sign_in(user) diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1cdbe4fc9a5..992580a6b34 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -32,6 +32,10 @@ FactoryGirl.define do request_access_enabled true end + trait :repository do + # no-op... for now! + end + trait :empty_repo do after(:create) do |project| project.create_repository diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb new file mode 100644 index 00000000000..12fc4ec4486 --- /dev/null +++ b/spec/factories/timelogs.rb @@ -0,0 +1,9 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :timelog do + time_spent 3600 + user + association :trackable, factory: :issue + end +end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 082b02116c0..85a8c263643 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -28,6 +28,10 @@ FactoryGirl.define do action { Todo::APPROVAL_REQUIRED } end + trait :unmergeable do + action { Todo::UNMERGEABLE } + end + trait :done do state :done end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index c16aafa1470..188d33e8ef4 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -125,7 +125,9 @@ describe 'Issue Boards', feature: true, js: true do first('.card').click end - page.within('.assignee') do + page.within(find('.assignee')) do + expect(page).to have_content('No assignee') + click_link 'assign yourself' wait_for_vue_resource diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index 6f6a2532c04..3ac9b2e0ae0 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -66,6 +66,12 @@ describe 'Dropdown assignee', js: true, feature: true do expect(dropdown_assignee_size).to eq(3) end + + it 'shows current user at top of dropdown' do + send_keys_to_filtered_search('assignee:') + + expect(first('#js-dropdown-assignee .filter-dropdown li')).to have_content(user.name) + end end describe 'filtering' do diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 60a86cc93d4..464749d01e3 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -66,6 +66,12 @@ describe 'Dropdown author', js: true, feature: true do expect(dropdown_author_size).to eq(3) end + + it 'shows current user at top of dropdown' do + send_keys_to_filtered_search('author:') + + expect(first('#js-dropdown-author li')).to have_content(user.name) + end end describe 'filtering' do diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 82c9bd0e6e6..31156fcf994 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -33,6 +33,45 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).not_to have_selector('.atwho-view') end + it 'doesnt select the first item for non-assignee dropdowns' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys('') + find('#note_note').native.send_keys(':') + end + + expect(page).to have_selector('.atwho-container') + + wait_for_ajax + + expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type') + end + + it 'selects the first item for assignee dropdowns' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys('') + find('#note_note').native.send_keys('@') + end + + expect(page).to have_selector('.atwho-container') + + wait_for_ajax + + expect(find('#at-view-64')).to have_selector('.cur:first-of-type') + end + + it 'selects the first item for non-assignee dropdowns if a query is entered' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys('') + find('#note_note').native.send_keys(':1') + end + + expect(page).to have_selector('.atwho-container') + + wait_for_ajax + + expect(find('#at-view-58')).to have_selector('.cur:first-of-type') + end + context 'if a selected value has special characters' do it 'wraps the result in double quotes' do note = find('#note_note') diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 31f75512f4a..0a9cd11ad6e 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -100,6 +100,58 @@ feature 'Issues > User uses slash commands', feature: true, js: true do end end + describe 'Issuable time tracking' do + let(:issue) { create(:issue, project: project) } + + before do + project.team << [user, :developer] + end + + context 'Issue' do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like 'issuable time tracker' + end + + context 'Merge Request' do + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it_behaves_like 'issuable time tracker' + end + end + + describe 'Issuable time tracking' do + let(:issue) { create(:issue, project: project) } + + before do + project.team << [user, :developer] + end + + context 'Issue' do + before do + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like 'issuable time tracker' + end + + context 'Merge Request' do + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it_behaves_like 'issuable time tracker' + end + end + describe 'toggling the WIP prefix from the title from note' do let(:issue) { create(:issue, project: project) } diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index c9a0059645d..4a6c76a5caf 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -22,4 +22,18 @@ feature 'Diffs URL', js: true, feature: true do expect(page).to have_css('.diffs.tab-pane.active') end end + + context 'when merge request has overflow' do + it 'displays warning' do + allow_any_instance_of(MergeRequestDiff).to receive(:overflow?).and_return(true) + allow(Commit).to receive(:max_diff_options).and_return(max_files: 20, max_lines: 20) + + visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + + page.within('.alert') do + expect(page).to have_text("Too many changes to show. Plain diff Email patch To preserve + performance only 3 of 3+ files are displayed.") + end + end + end end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index b1b3a47a1ce..b13674b4db9 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -68,6 +68,51 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do end end + describe 'merging the MR from the note' do + context 'when the current user can merge the MR' do + it 'merges the MR' do + write_note("/merge") + + expect(page).to have_content 'Commands applied' + + expect(merge_request.reload).to be_merged + end + end + + context 'when the head diff changes in the meanwhile' do + before do + merge_request.source_branch = 'another_branch' + merge_request.save + end + + it 'does not merge the MR' do + write_note("/merge") + + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload).not_to be_merged + end + end + + context 'when the current user cannot merge the MR' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + logout + login_with(guest) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not merge the MR' do + write_note("/merge") + + expect(page).not_to have_content 'Your commands have been executed!' + + expect(merge_request.reload).not_to be_merged + end + end + end + describe 'adding a due date from note' do it 'does not recognize the command nor create a note' do write_note('/due 2016-08-28') diff --git a/spec/features/merge_requests/wip_message_spec.rb b/spec/features/merge_requests/wip_message_spec.rb new file mode 100644 index 00000000000..3311731b33b --- /dev/null +++ b/spec/features/merge_requests/wip_message_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +feature 'Work In Progress help message', feature: true do + let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) } + let!(:user) { create(:user) } + + before do + project.team << [user, :master] + login_as(user) + end + + context 'with WIP commits' do + it 'shows a specific WIP hint' do + visit new_namespace_project_merge_request_path( + project.namespace, + project, + merge_request: { + source_project_id: project.id, + target_project_id: project.id, + source_branch: 'wip', + target_branch: 'master' + } + ) + + within_wip_explanation do + expect(page).to have_text( + 'It looks like you have some WIP commits in this branch' + ) + end + end + end + + context 'without WIP commits' do + it 'shows the regular WIP message' do + visit new_namespace_project_merge_request_path( + project.namespace, + project, + merge_request: { + source_project_id: project.id, + target_project_id: project.id, + source_branch: 'fix', + target_branch: 'master' + } + ) + + within_wip_explanation do + expect(page).not_to have_text( + 'It looks like you have some WIP commits in this branch' + ) + expect(page).to have_text( + "Start the title with WIP: to prevent a Work In Progress merge \ +request from being merged before it's ready" + ) + end + end + end + + def within_wip_explanation(&block) + page.within '.js-no-wip-explanation' do + yield + end + end +end diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index 8c4d4320dc5..11d27feab0b 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -3,6 +3,7 @@ require 'tempfile' feature 'Builds', :feature do let(:user) { create(:user) } + let(:user_access_level) { :developer } let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } @@ -14,7 +15,7 @@ feature 'Builds', :feature do end before do - project.team << [user, :developer] + project.team << [user, user_access_level] login_as(user) end @@ -131,7 +132,9 @@ feature 'Builds', :feature do context 'Artifacts expire date' do before do - build.update_attributes(artifacts_file: artifacts_file, artifacts_expire_at: expire_at) + build.update_attributes(artifacts_file: artifacts_file, + artifacts_expire_at: expire_at) + visit namespace_project_build_path(project.namespace, project, build) end @@ -146,12 +149,23 @@ feature 'Builds', :feature do context 'when expire date is defined' do let(:expire_at) { Time.now + 7.days } - it 'keeps artifacts when Keep button is clicked' do - expect(page).to have_content 'The artifacts will be removed' - click_link 'Keep' + context 'when user has ability to update build' do + it 'keeps artifacts when keep button is clicked' do + expect(page).to have_content 'The artifacts will be removed' - expect(page).not_to have_link 'Keep' - expect(page).not_to have_content 'The artifacts will be removed' + click_link 'Keep' + + expect(page).to have_no_link 'Keep' + expect(page).to have_no_content 'The artifacts will be removed' + end + end + + context 'when user does not have ability to update build' do + let(:user_access_level) { :guest } + + it 'does not have keep button' do + expect(page).to have_no_link 'Keep' + end end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 14e009daba8..e673ece37c3 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -11,18 +11,42 @@ describe 'Pipeline', :feature, :js do project.team << [user, :developer] end + shared_context 'pipeline builds' do + let!(:build_passed) do + create(:ci_build, :success, + pipeline: pipeline, stage: 'build', name: 'build') + end + + let!(:build_failed) do + create(:ci_build, :failed, + pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') + end + + let!(:build_running) do + create(:ci_build, :running, + pipeline: pipeline, stage: 'deploy', name: 'deploy') + end + + let!(:build_manual) do + create(:ci_build, :manual, + pipeline: pipeline, stage: 'deploy', name: 'manual-build') + end + + let!(:build_external) do + create(:generic_commit_status, status: 'success', + pipeline: pipeline, + name: 'jenkins', + stage: 'external', + target_url: 'http://gitlab.com/status') + end + end + describe 'GET /:project/pipelines/:id' do + include_context 'pipeline builds' + let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } - before do - @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') - @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') - @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') - @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual-build') - @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') - end - before { visit namespace_project_pipeline_path(project.namespace, project, pipeline) } it 'shows the pipeline graph' do @@ -116,6 +140,7 @@ describe 'Pipeline', :feature, :js do it 'shows the success icon and the generic comit status build' do expect(page).to have_selector('.ci-status-icon-success') expect(page).to have_content('jenkins') + expect(page).to have_link('jenkins', href: 'http://gitlab.com/status') end end end @@ -157,26 +182,22 @@ describe 'Pipeline', :feature, :js do end describe 'GET /:project/pipelines/:id/builds' do + include_context 'pipeline builds' + let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id) } before do - @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') - @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') - @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') - @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual-build') - @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') + visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline) end - before { visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline)} - it 'shows a list of builds' do expect(page).to have_content('Test') - expect(page).to have_content(@success.id) + expect(page).to have_content(build_passed.id) expect(page).to have_content('Deploy') - expect(page).to have_content(@failed.id) - expect(page).to have_content(@running.id) - expect(page).to have_content(@external.id) + expect(page).to have_content(build_failed.id) + expect(page).to have_content(build_running.id) + expect(page).to have_content(build_external.id) expect(page).to have_content('Retry failed') expect(page).to have_content('Cancel running') expect(page).to have_link('Play') @@ -230,7 +251,7 @@ describe 'Pipeline', :feature, :js do end end - it { expect(@manual.reload).to be_pending } + it { expect(build_manual.reload).to be_pending } end end end diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb new file mode 100644 index 00000000000..cef315ac9cd --- /dev/null +++ b/spec/features/projects/settings/visibility_settings_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +feature 'Visibility settings', feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace, visibility_level: 20) } + + context 'as owner' do + before do + login_as(user) + visit edit_namespace_project_path(project.namespace, project) + end + + scenario 'project visibility select is available' do + visibility_select_container = find('.js-visibility-select') + + expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s + expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.' + end + + scenario 'project visibility description updates on change' do + visibility_select_container = find('.js-visibility-select') + visibility_select = visibility_select_container.find('.visibility-select') + visibility_select.select('Private') + + expect(visibility_select.value).to eq '0' + expect(visibility_select_container).to have_content 'Project access must be granted explicitly to each user.' + end + end + + context 'as master' do + let(:master_user) { create(:user) } + + before do + project.team << [master_user, :master] + login_as(master_user) + visit edit_namespace_project_path(project.namespace, project) + end + + scenario 'project visibility is locked' do + visibility_select_container = find('.js-visibility-select') + + expect(visibility_select_container).not_to have_select '.visibility-select' + expect(visibility_select_container).to have_content 'Public' + expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.' + end + end +end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 903224589dd..1f221487393 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -62,4 +62,19 @@ describe MergeRequestsHelper do it { is_expected.to eq([source_title, target_title]) } end end + + describe 'mr_widget_refresh_url' do + let(:merge_request) { create(:merge_request, source_project: project) } + let(:project) { create(:project) } + + it 'returns correct url for MR' do + expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh" + + expect(mr_widget_refresh_url(merge_request)).to end_with(expected_url) + end + + it 'returns empty string for nil' do + expect(mr_widget_refresh_url(nil)).to end_with('') + end + end end diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index b3617a45bd4..7bc5b3268a0 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */ +/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */ /*= require js.cookie.js */ /*= require jquery.endless-scroll.js */ @@ -19,18 +19,18 @@ name: 'merge events', }, { id: 'comments', - },{ + }, { id: 'team', }]; function getEventName(index) { - let filter = filters[index]; + const filter = filters[index]; return filter.hasOwnProperty('name') ? filter.name : filter.id; } function getSelector(index) { - let filter = filters[index]; - return `#${filter.id}_event_filter` + const filter = filters[index]; + return `#${filter.id}_event_filter`; } describe('Activities', () => { @@ -39,17 +39,17 @@ new gl.Activities(); }); - for(let i = 0; i < filters.length; i++) { + for (let i = 0; i < filters.length; i += 1) { ((i) => { describe(`when selecting ${getEventName(i)}`, () => { beforeEach(() => { $(getSelector(i)).click(); }); - for(let x = 0; x < filters.length; x++) { + for (let x = 0; x < filters.length; x += 1) { ((x) => { - let shouldHighlight = i === x; - let testName = shouldHighlight ? 'should highlight' : 'should not highlight'; + const shouldHighlight = i === x; + const testName = shouldHighlight ? 'should highlight' : 'should not highlight'; it(`${testName} ${getEventName(x)}`, () => { expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index faba2837d41..71446b9df61 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */ /* global AwardsHandler */ /*= require awards_handler */ @@ -231,5 +231,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index e77d732a32a..51d911792ba 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */ /*= require behaviors/autosize */ @@ -18,5 +18,4 @@ return $(document).trigger('page:load'); }; }); - }).call(this); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 1a1f34cfdc0..0f046c2d83a 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */ /*= require behaviors/quick_submit */ @@ -93,5 +93,4 @@ return $.Event('keydown', $.extend({}, defaults, options)); }; }); - }).call(this); diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 1f62591c06d..9467056f04c 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, padded-blocks */ +/* eslint-disable space-before-function-paren, no-var */ /*= require behaviors/requires_input */ @@ -41,5 +41,4 @@ return expect(spy).toHaveBeenCalled(); }); }); - }).call(this); diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index b3a1afa28a5..7c5850111cb 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, one-var, no-unused-vars, indent */ +/* eslint-disable comma-dangle, one-var, no-unused-vars */ /* global Vue */ /* global BoardService */ /* global boardsMockInterceptor */ @@ -146,8 +146,8 @@ describe('Store', () => { }); it('moves the position of lists', () => { - const listOne = gl.issueBoards.BoardsStore.addList(listObj), - listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + const listOne = gl.issueBoards.BoardsStore.addList(listObj); + const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); @@ -157,8 +157,8 @@ describe('Store', () => { }); it('moves an issue from one list to another', (done) => { - const listOne = gl.issueBoards.BoardsStore.addList(listObj), - listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); + const listOne = gl.issueBoards.BoardsStore.addList(listObj); + const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate); expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 3f6b328348d..4d851b2d320 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-new, padded-blocks */ +/* eslint-disable no-new */ /*= require sidebar */ /*= require jquery */ @@ -36,5 +36,4 @@ expect(todosCountText()).toEqual('1,000,000'); }); }); - })(window.gl); diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index 18805d26ac0..fbfa34a5da7 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ +/* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ /* global CommentsStore */ //= require vue @@ -9,7 +9,7 @@ (() => { function createDiscussion(noteId = 1, resolved = true) { CommentsStore.create('a', noteId, true, resolved, 'test'); - }; + } beforeEach(() => { CommentsStore.state = {}; diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 index 2ec759c8e80..3949c5615d5 100644 --- a/spec/javascripts/extensions/array_spec.js.es6 +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, padded-blocks */ +/* eslint-disable space-before-function-paren, no-var */ /*= require extensions/array */ @@ -42,5 +42,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index 91846bb9143..5cd0e5ab0f0 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, padded-blocks */ +/* eslint-disable space-before-function-paren, no-var */ /*= require extensions/jquery */ @@ -39,5 +39,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js index 3d776bb9277..2ef242901e8 100644 --- a/spec/javascripts/fixtures/emoji_menu.js +++ b/spec/javascripts/fixtures/emoji_menu.js @@ -1,5 +1,4 @@ -/* eslint-disable space-before-function-paren, padded-blocks */ +/* eslint-disable space-before-function-paren */ (function() { window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>"; - }).call(this); diff --git a/spec/javascripts/gfm_auto_complete_spec.js.es6 b/spec/javascripts/gfm_auto_complete_spec.js.es6 new file mode 100644 index 00000000000..6b48d82cb23 --- /dev/null +++ b/spec/javascripts/gfm_auto_complete_spec.js.es6 @@ -0,0 +1,65 @@ +//= require gfm_auto_complete +//= require jquery +//= require jquery.atwho + +const global = window.gl || (window.gl = {}); +const GfmAutoComplete = global.GfmAutoComplete; + +describe('GfmAutoComplete', function () { + describe('DefaultOptions.sorter', function () { + describe('assets loading', function () { + beforeEach(function () { + spyOn(GfmAutoComplete, 'isLoading').and.returnValue(true); + + this.atwhoInstance = { setting: {} }; + this.items = []; + + this.sorterValue = GfmAutoComplete.DefaultOptions.sorter + .call(this.atwhoInstance, '', this.items); + }); + + it('should disable highlightFirst', function () { + expect(this.atwhoInstance.setting.highlightFirst).toBe(false); + }); + + it('should return the passed unfiltered items', function () { + expect(this.sorterValue).toEqual(this.items); + }); + }); + + describe('assets finished loading', function () { + beforeEach(function () { + spyOn(GfmAutoComplete, 'isLoading').and.returnValue(false); + spyOn($.fn.atwho.default.callbacks, 'sorter'); + }); + + it('should enable highlightFirst if alwaysHighlightFirst is set', function () { + const atwhoInstance = { setting: { alwaysHighlightFirst: true } }; + + GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance); + + expect(atwhoInstance.setting.highlightFirst).toBe(true); + }); + + it('should enable highlightFirst if a query is present', function () { + const atwhoInstance = { setting: {} }; + + GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, 'query'); + + expect(atwhoInstance.setting.highlightFirst).toBe(true); + }); + + it('should call the default atwho sorter', function () { + const atwhoInstance = { setting: {} }; + + const query = 'query'; + const items = []; + const searchKey = 'searchKey'; + + GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, query, items, searchKey); + + expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey); + }); + }); + }); +}); diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index d11b1182d9a..06fa64b1b4e 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */ +/* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */ /* global Turbolinks */ /*= require jquery */ @@ -22,7 +22,7 @@ let remoteCallback; - let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) { + const navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) { i = i || 0; if (!i) direction = direction.toUpperCase(); $('body').trigger({ @@ -30,7 +30,7 @@ which: ARROW_KEYS[direction], keyCode: ARROW_KEYS[direction] }); - i++; + i += 1; if (i <= steps) { navigateWithKeys(direction, steps, cb, i); } else { @@ -38,9 +38,9 @@ } }; - let remoteMock = function remoteMock(data, term, callback) { + const remoteMock = function remoteMock(data, term, callback) { remoteCallback = callback.bind({}, data); - } + }; describe('Dropdown', function describeDropdown() { preloadFixtures('static/gl_dropdown.html.raw'); @@ -89,7 +89,7 @@ it('should select a following item on DOWN keypress', () => { expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); - let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0); + const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0); navigateWithKeys('down', randomIndex, () => { expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); @@ -100,7 +100,7 @@ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0); navigateWithKeys('down', (this.projectsData.length - 1), () => { expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); - let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0); + const randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0); navigateWithKeys('up', randomIndex, () => { expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1); expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused'); @@ -109,15 +109,15 @@ }); it('should click the selected item on ENTER keypress', () => { - expect(this.dropdownContainerElement).toHaveClass('open') - let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0 + expect(this.dropdownContainerElement).toHaveClass('open'); + const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; navigateWithKeys('down', randomIndex, () => { spyOn(Turbolinks, 'visit').and.stub(); navigateWithKeys('enter', null, () => { expect(this.dropdownContainerElement).not.toHaveClass('open'); - let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); + const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); expect(link).toHaveClass('is-active'); - let linkedLocation = link.attr('href'); + const linkedLocation = link.attr('href'); if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation); }); }); @@ -140,18 +140,18 @@ this.dropdownButtonElement.click(); }); - it('should not focus search input while remote task is not complete', ()=> { + it('should not focus search input while remote task is not complete', () => { expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR)); remoteCallback(); expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); - it('should focus search input after remote task is complete', ()=> { + it('should focus search input after remote task is complete', () => { remoteCallback(); expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); - it('should focus on input when opening for the second time', ()=> { + it('should focus on input when opening for the second time', () => { remoteCallback(); this.dropdownContainerElement.trigger({ type: 'keyup', @@ -164,16 +164,15 @@ }); describe('input focus with array data', () => { - it('should focus input when passing array data to drop down', ()=> { + it('should focus input when passing array data to drop down', () => { initDropDown.call(this, false, true); this.dropdownButtonElement.click(); expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); }); - it('should still have input value on close and restore', () => { - let $searchInput = $(SEARCH_INPUT_SELECTOR); + const $searchInput = $(SEARCH_INPUT_SELECTOR); initDropDown.call(this, false, true); $searchInput .trigger('focus') diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index e5d934540af..f68fd9e00d7 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */ +/* eslint-disable space-before-function-paren, arrow-body-style */ //= require jquery //= require gl_field_errors @@ -28,7 +28,7 @@ expect(customErrorElem.length).toBe(1); const customErrors = this.fieldErrors.state.inputs.filter((input) => { - return input.inputElement.hasClass(customErrorFlag); + return input.inputElement.hasClass(customErrorFlag); }); expect(customErrors.length).toBe(0); }); @@ -107,7 +107,5 @@ expect(noTitleErrorElem.text()).toBe('This field is required.'); expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.'); }); - }); - })(window.gl || (window.gl = {})); diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index bc5cbeb6a40..d76fcc5206a 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable quotes, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */ +/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var, max-len */ /* global d3 */ /* global ContributorsGraph */ /* global ContributorsMasterGraph */ @@ -8,126 +8,123 @@ describe("ContributorsGraph", function () { describe("#set_x_domain", function () { it("set the x_domain", function () { - ContributorsGraph.set_x_domain(20) - expect(ContributorsGraph.prototype.x_domain).toEqual(20) - }) - }) + ContributorsGraph.set_x_domain(20); + expect(ContributorsGraph.prototype.x_domain).toEqual(20); + }); + }); describe("#set_y_domain", function () { it("sets the y_domain", function () { - ContributorsGraph.set_y_domain([{commits: 30}]) - expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]) - }) - }) + ContributorsGraph.set_y_domain([{ commits: 30 }]); + expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]); + }); + }); describe("#init_x_domain", function () { it("sets the initial x_domain", function () { - ContributorsGraph.init_x_domain([{date: "2013-01-31"}, {date: "2012-01-31"}]) - expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"]) - }) - }) + ContributorsGraph.init_x_domain([{ date: "2013-01-31" }, { date: "2012-01-31" }]); + expect(ContributorsGraph.prototype.x_domain).toEqual(["2012-01-31", "2013-01-31"]); + }); + }); describe("#init_y_domain", function () { it("sets the initial y_domain", function () { - ContributorsGraph.init_y_domain([{commits: 30}]) - expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]) - }) - }) + ContributorsGraph.init_y_domain([{ commits: 30 }]); + expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]); + }); + }); describe("#init_domain", function () { it("calls init_x_domain and init_y_domain", function () { - spyOn(ContributorsGraph, "init_x_domain") - spyOn(ContributorsGraph, "init_y_domain") - ContributorsGraph.init_domain() - expect(ContributorsGraph.init_x_domain).toHaveBeenCalled() - expect(ContributorsGraph.init_y_domain).toHaveBeenCalled() - }) - }) + spyOn(ContributorsGraph, "init_x_domain"); + spyOn(ContributorsGraph, "init_y_domain"); + ContributorsGraph.init_domain(); + expect(ContributorsGraph.init_x_domain).toHaveBeenCalled(); + expect(ContributorsGraph.init_y_domain).toHaveBeenCalled(); + }); + }); describe("#set_dates", function () { it("sets the dates", function () { - ContributorsGraph.set_dates("2013-12-01") - expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01") - }) - }) + ContributorsGraph.set_dates("2013-12-01"); + expect(ContributorsGraph.prototype.dates).toEqual("2013-12-01"); + }); + }); describe("#set_x_domain", function () { it("sets the instance's x domain using the prototype's x_domain", function () { - ContributorsGraph.prototype.x_domain = 20 - var instance = new ContributorsGraph() - instance.x = d3.time.scale().range([0, 100]).clamp(true) - spyOn(instance.x, 'domain') - instance.set_x_domain() - expect(instance.x.domain).toHaveBeenCalledWith(20) - }) - }) + ContributorsGraph.prototype.x_domain = 20; + var instance = new ContributorsGraph(); + instance.x = d3.time.scale().range([0, 100]).clamp(true); + spyOn(instance.x, 'domain'); + instance.set_x_domain(); + expect(instance.x.domain).toHaveBeenCalledWith(20); + }); + }); describe("#set_y_domain", function () { it("sets the instance's y domain using the prototype's y_domain", function () { - ContributorsGraph.prototype.y_domain = 30 - var instance = new ContributorsGraph() - instance.y = d3.scale.linear().range([100, 0]).nice() - spyOn(instance.y, 'domain') - instance.set_y_domain() - expect(instance.y.domain).toHaveBeenCalledWith(30) - }) - }) + ContributorsGraph.prototype.y_domain = 30; + var instance = new ContributorsGraph(); + instance.y = d3.scale.linear().range([100, 0]).nice(); + spyOn(instance.y, 'domain'); + instance.set_y_domain(); + expect(instance.y.domain).toHaveBeenCalledWith(30); + }); + }); describe("#set_domain", function () { it("calls set_x_domain and set_y_domain", function () { - var instance = new ContributorsGraph() - spyOn(instance, 'set_x_domain') - spyOn(instance, 'set_y_domain') - instance.set_domain() - expect(instance.set_x_domain).toHaveBeenCalled() - expect(instance.set_y_domain).toHaveBeenCalled() - }) - }) + var instance = new ContributorsGraph(); + spyOn(instance, 'set_x_domain'); + spyOn(instance, 'set_y_domain'); + instance.set_domain(); + expect(instance.set_x_domain).toHaveBeenCalled(); + expect(instance.set_y_domain).toHaveBeenCalled(); + }); + }); describe("#set_data", function () { it("sets the data", function () { - var instance = new ContributorsGraph() - instance.set_data("20") - expect(instance.data).toEqual("20") - }) - }) -}) + var instance = new ContributorsGraph(); + instance.set_data("20"); + expect(instance.data).toEqual("20"); + }); + }); +}); describe("ContributorsMasterGraph", function () { - // TODO: fix or remove - //describe("#process_dates", function () { - //it("gets and parses dates", function () { - //var graph = new ContributorsMasterGraph() - //var data = 'random data here' - //spyOn(graph, 'parse_dates') - //spyOn(graph, 'get_dates').andReturn("get") - //spyOn(ContributorsGraph,'set_dates').andCallThrough() - //graph.process_dates(data) - //expect(graph.parse_dates).toHaveBeenCalledWith(data) - //expect(graph.get_dates).toHaveBeenCalledWith(data) - //expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get") - //}) - //}) + // describe("#process_dates", function () { + // it("gets and parses dates", function () { + // var graph = new ContributorsMasterGraph(); + // var data = 'random data here'; + // spyOn(graph, 'parse_dates'); + // spyOn(graph, 'get_dates').andReturn("get"); + // spyOn(ContributorsGraph,'set_dates').andCallThrough(); + // graph.process_dates(data); + // expect(graph.parse_dates).toHaveBeenCalledWith(data); + // expect(graph.get_dates).toHaveBeenCalledWith(data); + // expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get"); + // }); + // }); describe("#get_dates", function () { it("plucks the date field from data collection", function () { - var graph = new ContributorsMasterGraph() - var data = [{date: "2013-01-01"}, {date: "2012-12-15"}] - expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"]) - }) - }) + var graph = new ContributorsMasterGraph(); + var data = [{ date: "2013-01-01" }, { date: "2012-12-15" }]; + expect(graph.get_dates(data)).toEqual(["2013-01-01", "2012-12-15"]); + }); + }); describe("#parse_dates", function () { it("parses the dates", function () { - var graph = new ContributorsMasterGraph() - var parseDate = d3.time.format("%Y-%m-%d").parse - var data = [{date: "2013-01-01"}, {date: "2012-12-15"}] - var correct = [{date: parseDate(data[0].date)}, {date: parseDate(data[1].date)}] - graph.parse_dates(data) - expect(data).toEqual(correct) - }) - }) - - -}) + var graph = new ContributorsMasterGraph(); + var parseDate = d3.time.format("%Y-%m-%d").parse; + var data = [{ date: "2013-01-01" }, { date: "2012-12-15" }]; + var correct = [{ date: parseDate(data[0].date) }, { date: parseDate(data[1].date) }]; + graph.parse_dates(data); + expect(data).toEqual(correct); + }); + }); +}); diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 751f3d175e2..63f28dfb8ad 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,215 +1,218 @@ -/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */ +/* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */ /* global ContributorsStatGraphUtil */ //= require graphs/stat_graph_contributors_util describe("ContributorsStatGraphUtil", function () { - describe("#parse_log", function () { it("returns a correctly parsed log", function () { var fake_log = [ - {author_email: "karlo@email.com", author_name: "Karlo Soriano", date: "2013-05-09", additions: 471}, - {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1}, - {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3}, - {author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}] + { author_email: "karlo@email.com", author_name: "Karlo Soriano", date: "2013-05-09", additions: 471 }, + { author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1 }, + { author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3 }, + { author_email: "dzaporozhets@email.com", author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3 } + ]; var correct_parsed_log = { total: [ - {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, - {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], - by_author: - [ - { - author_name: "Karlo Soriano", author_email: "karlo@email.com", - "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} - }, - { - author_name: "Dmitriy Zaporozhets",author_email: "dzaporozhets@email.com", - "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} - } + { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }, + { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 } + ], + by_author: [ + { + author_name: "Karlo Soriano", author_email: "karlo@email.com", + "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 } + }, + { + author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com", + "2013-05-08": { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 } + } ] - } - expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log) - }) - }) + }; + expect(ContributorsStatGraphUtil.parse_log(fake_log)).toEqual(correct_parsed_log); + }); + }); describe("#store_data", function () { - - var fake_entry = {author: "Karlo Soriano", date: "2013-05-09", additions: 471} - var fake_total = {} - var fake_by_author = {} + var fake_entry = { author: "Karlo Soriano", date: "2013-05-09", additions: 471 }; + var fake_total = {}; + var fake_by_author = {}; it("calls #store_commits", function () { - spyOn(ContributorsStatGraphUtil, 'store_commits') - ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) - expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled() - }) + spyOn(ContributorsStatGraphUtil, 'store_commits'); + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author); + expect(ContributorsStatGraphUtil.store_commits).toHaveBeenCalled(); + }); it("calls #store_additions", function () { - spyOn(ContributorsStatGraphUtil, 'store_additions') - ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) - expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled() - }) + spyOn(ContributorsStatGraphUtil, 'store_additions'); + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author); + expect(ContributorsStatGraphUtil.store_additions).toHaveBeenCalled(); + }); it("calls #store_deletions", function () { - spyOn(ContributorsStatGraphUtil, 'store_deletions') - ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author) - expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled() - }) - - }) + spyOn(ContributorsStatGraphUtil, 'store_deletions'); + ContributorsStatGraphUtil.store_data(fake_entry, fake_total, fake_by_author); + expect(ContributorsStatGraphUtil.store_deletions).toHaveBeenCalled(); + }); + }); // TODO: fix or remove - //describe("#store_commits", function () { - //var fake_total = "fake_total" - //var fake_by_author = "fake_by_author" - - //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { - //spyOn(ContributorsStatGraphUtil, 'add') - //ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author) - //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]]) - //}) - //}) + // describe("#store_commits", function () { + // var fake_total = "fake_total"; + // var fake_by_author = "fake_by_author"; + // + // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + // spyOn(ContributorsStatGraphUtil, 'add'); + // ContributorsStatGraphUtil.store_commits(fake_total, fake_by_author); + // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "commits", 1], ["fake_by_author", "commits", 1]]); + // }); + // }); describe("#add", function () { it("adds 1 to current test_field in collection", function () { - var fake_collection = {test_field: 10} - ContributorsStatGraphUtil.add(fake_collection, "test_field", 1) - expect(fake_collection.test_field).toEqual(11) - }) + var fake_collection = { test_field: 10 }; + ContributorsStatGraphUtil.add(fake_collection, "test_field", 1); + expect(fake_collection.test_field).toEqual(11); + }); it("inits and adds 1 if test_field in collection is not defined", function () { - var fake_collection = {} - ContributorsStatGraphUtil.add(fake_collection, "test_field", 1) - expect(fake_collection.test_field).toEqual(1) - }) - }) + var fake_collection = {}; + ContributorsStatGraphUtil.add(fake_collection, "test_field", 1); + expect(fake_collection.test_field).toEqual(1); + }); + }); // TODO: fix or remove - //describe("#store_additions", function () { - //var fake_entry = {additions: 10} - //var fake_total= "fake_total" - //var fake_by_author = "fake_by_author" - //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { - //spyOn(ContributorsStatGraphUtil, 'add') - //ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author) - //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]]) - //}) - //}) + // describe("#store_additions", function () { + // var fake_entry = {additions: 10}; + // var fake_total= "fake_total"; + // var fake_by_author = "fake_by_author"; + // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + // spyOn(ContributorsStatGraphUtil, 'add'); + // ContributorsStatGraphUtil.store_additions(fake_entry, fake_total, fake_by_author); + // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "additions", 10], ["fake_by_author", "additions", 10]]); + // }); + // }); // TODO: fix or remove - //describe("#store_deletions", function () { - //var fake_entry = {deletions: 10} - //var fake_total= "fake_total" - //var fake_by_author = "fake_by_author" - //it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { - //spyOn(ContributorsStatGraphUtil, 'add') - //ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author) - //expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]]) - //}) - //}) + // describe("#store_deletions", function () { + // var fake_entry = {deletions: 10}; + // var fake_total= "fake_total"; + // var fake_by_author = "fake_by_author"; + // it("calls #add twice with arguments fake_total and fake_by_author respectively", function () { + // spyOn(ContributorsStatGraphUtil, 'add'); + // ContributorsStatGraphUtil.store_deletions(fake_entry, fake_total, fake_by_author); + // expect(ContributorsStatGraphUtil.add.argsForCall).toEqual([["fake_total", "deletions", 10], ["fake_by_author", "deletions", 10]]); + // }); + // }); describe("#add_date", function () { it("adds a date field to the collection", function () { - var fake_date = "2013-10-02" - var fake_collection = {} - ContributorsStatGraphUtil.add_date(fake_date, fake_collection) - expect(fake_collection[fake_date].date).toEqual("2013-10-02") - }) - }) + var fake_date = "2013-10-02"; + var fake_collection = {}; + ContributorsStatGraphUtil.add_date(fake_date, fake_collection); + expect(fake_collection[fake_date].date).toEqual("2013-10-02"); + }); + }); describe("#add_author", function () { it("adds an author field to the collection", function () { - var fake_author = { author_name: "Author", author_email: 'fake@email.com' } - var fake_author_collection = {} - var fake_email_collection = {} - ContributorsStatGraphUtil.add_author(fake_author, fake_author_collection, fake_email_collection) - expect(fake_author_collection[fake_author.author_name].author_name).toEqual("Author") - expect(fake_email_collection[fake_author.author_email].author_name).toEqual("Author") - }) - }) + var fake_author = { author_name: "Author", author_email: 'fake@email.com' }; + var fake_author_collection = {}; + var fake_email_collection = {}; + ContributorsStatGraphUtil.add_author(fake_author, fake_author_collection, fake_email_collection); + expect(fake_author_collection[fake_author.author_name].author_name).toEqual("Author"); + expect(fake_email_collection[fake_author.author_email].author_name).toEqual("Author"); + }); + }); describe("#get_total_data", function () { it("returns the collection sorted via specified field", function () { var fake_parsed_log = { - total: [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, - {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}], - by_author:[ - { - author: "Karlo Soriano", - "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} - }, - { - author: "Dmitriy Zaporozhets", - "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} - } - ]}; - var correct_total_data = [{date: "2013-05-08", commits: 3}, - {date: "2013-05-09", commits: 1}]; - expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data) - }) - }) + total: [ + { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }, + { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 } + ], + by_author: [ + { + author: "Karlo Soriano", + "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 } + }, + { + author: "Dmitriy Zaporozhets", + "2013-05-08": { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 } + } + ] + }; + var correct_total_data = [ + { date: "2013-05-08", commits: 3 }, + { date: "2013-05-09", commits: 1 } + ]; + expect(ContributorsStatGraphUtil.get_total_data(fake_parsed_log, "commits")).toEqual(correct_total_data); + }); + }); describe("#pick_field", function () { it("returns the collection with only the specified field and date", function () { - var fake_parsed_log_total = [{date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, - {date: "2013-05-08", additions: 54, deletions: 7, commits: 3}]; - ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits") - var correct_pick_field_data = [{date: "2013-05-09", commits: 1},{date: "2013-05-08", commits: 3}]; - expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data) - }) - }) + var fake_parsed_log_total = [ + { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }, + { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 } + ]; + ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits"); + var correct_pick_field_data = [{ date: "2013-05-09", commits: 1 }, { date: "2013-05-08", commits: 3 }]; + expect(ContributorsStatGraphUtil.pick_field(fake_parsed_log_total, "commits")).toEqual(correct_pick_field_data); + }); + }); describe("#get_author_data", function () { it("returns the log by author sorted by specified field", function () { var fake_parsed_log = { total: [ - {date: "2013-05-09", additions: 471, deletions: 0, commits: 1}, - {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 }, + { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 } ], by_author: [ { author_name: "Karlo Soriano", author_email: "karlo@email.com", - "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} + "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 } }, { author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com", - "2013-05-08": {date: "2013-05-08", additions: 54, deletions: 7, commits: 3} + "2013-05-08": { date: "2013-05-08", additions: 54, deletions: 7, commits: 3 } } ] - } + }; var correct_author_data = [ - {author_name:"Dmitriy Zaporozhets",author_email:"dzaporozhets@email.com",dates:{"2013-05-08":3},deletions:7,additions:54,"commits":3}, - {author_name:"Karlo Soriano",author_email:"karlo@email.com",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1} - ] - expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data) - }) - }) + { author_name: "Dmitriy Zaporozhets", author_email: "dzaporozhets@email.com", dates: { "2013-05-08": 3 }, deletions: 7, additions: 54, "commits": 3 }, + { author_name: "Karlo Soriano", author_email: "karlo@email.com", dates: { "2013-05-09": 1 }, deletions: 0, additions: 471, commits: 1 } + ]; + expect(ContributorsStatGraphUtil.get_author_data(fake_parsed_log, "commits")).toEqual(correct_author_data); + }); + }); describe("#parse_log_entry", function () { it("adds the corresponding info from the log entry to the author", function () { - var fake_log_entry = { author_name: "Karlo Soriano", author_email: "karlo@email.com", - "2013-05-09": {date: "2013-05-09", additions: 471, deletions: 0, commits: 1} - } - var correct_parsed_log = {author_name:"Karlo Soriano",author_email:"karlo@email.com",dates:{"2013-05-09":1},deletions:0,additions:471,commits:1} - expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log) - }) - }) + var fake_log_entry = { author_name: "Karlo Soriano", author_email: "karlo@email.com", + "2013-05-09": { date: "2013-05-09", additions: 471, deletions: 0, commits: 1 } + }; + var correct_parsed_log = { author_name: "Karlo Soriano", author_email: "karlo@email.com", dates: { "2013-05-09": 1 }, deletions: 0, additions: 471, commits: 1 }; + expect(ContributorsStatGraphUtil.parse_log_entry(fake_log_entry, 'commits', null)).toEqual(correct_parsed_log); + }); + }); describe("#in_range", function () { - var date = "2013-05-09" + var date = "2013-05-09"; it("returns true if date_range is null", function () { - expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true) - }) + expect(ContributorsStatGraphUtil.in_range(date, null)).toEqual(true); + }); it("returns true if date is in range", function () { - var date_range = [new Date("2013-01-01"), new Date("2013-12-12")] - expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true) - }) + var date_range = [new Date("2013-01-01"), new Date("2013-12-12")]; + expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(true); + }); it("returns false if date is not in range", function () { - var date_range = [new Date("1999-12-01"), new Date("2000-12-01")] - expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false) - }) - }) - - -}) + var date_range = [new Date("1999-12-01"), new Date("2000-12-01")]; + expect(ContributorsStatGraphUtil.in_range(date, date_range)).toEqual(false); + }); + }); +}); diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index 0da124632ae..71b589e6b83 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,10 +1,9 @@ -/* eslint-disable quotes, padded-blocks, semi */ +/* eslint-disable quotes */ /* global StatGraph */ //= require graphs/stat_graph describe("StatGraph", function () { - describe("#get_log", function () { it("returns log", function () { StatGraph.log = "test"; @@ -16,7 +15,6 @@ describe("StatGraph", function () { it("sets the log", function () { StatGraph.set_log("test"); expect(StatGraph.log).toBe("test"); - }) - }) - + }); + }); }); diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index b5262afa1cf..b846c5ab00b 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -1,10 +1,9 @@ -/* eslint-disable space-before-function-paren, padded-blocks, no-var */ +/* eslint-disable space-before-function-paren, no-var */ /*= require header */ /*= require lib/utils/text_utility */ /*= require jquery */ (function() { - describe('Header', function() { var todosPendingCount = '.todos-pending-count'; var fixtureTemplate = 'static/header.html.raw'; @@ -51,5 +50,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6 new file mode 100644 index 00000000000..a1e979e8d09 --- /dev/null +++ b/spec/javascripts/issuable_time_tracker_spec.js.es6 @@ -0,0 +1,201 @@ +/* eslint-disable */ +//= require jquery +//= require vue +//= require issuable/time_tracking/components/time_tracker + +function initTimeTrackingComponent(opts) { + fixture.set(` + <div> + <div id="mock-container"></div> + </div> + `); + + this.initialData = { + time_estimate: opts.timeEstimate, + time_spent: opts.timeSpent, + human_time_estimate: opts.timeEstimateHumanReadable, + human_time_spent: opts.timeSpentHumanReadable, + docsUrl: '/help/workflow/time_tracking.md', + }; + + const TimeTrackingComponent = Vue.component('issuable-time-tracker'); + this.timeTracker = new TimeTrackingComponent({ + el: '#mock-container', + propsData: this.initialData, + }); +} + +((gl) => { + describe('Issuable Time Tracker', function() { + describe('Initialization', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' }); + }); + + it('should return something defined', function() { + expect(this.timeTracker).toBeDefined(); + }); + + it ('should correctly set timeEstimate', function(done) { + Vue.nextTick(() => { + expect(this.timeTracker.timeEstimate).toBe(this.initialData.time_estimate); + done(); + }); + }); + it ('should correctly set time_spent', function(done) { + Vue.nextTick(() => { + expect(this.timeTracker.timeSpent).toBe(this.initialData.time_spent); + done(); + }); + }); + }); + + describe('Content Display', function() { + describe('Panes', function() { + describe('Comparison pane', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 5000, timeEstimateHumanReadable: '', timeSpentHumanReadable: '' }); + }); + + it('should show the "Comparison" pane when timeEstimate and time_spent are truthy', function(done) { + Vue.nextTick(() => { + const $comparisonPane = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane'); + expect(this.timeTracker.showComparisonState).toBe(true); + done(); + }); + }); + + describe('Remaining meter', function() { + it('should display the remaining meter with the correct width', function(done) { + Vue.nextTick(() => { + const meterWidth = this.timeTracker.$el.querySelector('.time-tracking-comparison-pane .meter-fill').style.width; + const correctWidth = '5%'; + + expect(meterWidth).toBe(correctWidth); + done(); + }) + }); + + it('should display the remaining meter with the correct background color when within estimate', function(done) { + Vue.nextTick(() => { + const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .within_estimate .meter-fill'); + expect(styledMeter.length).toBe(1); + done() + }); + }); + + it('should display the remaining meter with the correct background color when over estimate', function(done) { + this.timeTracker.time_estimate = 100000; + this.timeTracker.time_spent = 20000000; + Vue.nextTick(() => { + const styledMeter = $(this.timeTracker.$el).find('.time-tracking-comparison-pane .over_estimate .meter-fill'); + expect(styledMeter.length).toBe(1); + done(); + }); + }); + }); + }); + + describe("Estimate only pane", function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 100000, timeSpent: 0, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '' }); + }); + + it('should display the human readable version of time estimated', function(done) { + Vue.nextTick(() => { + const estimateText = this.timeTracker.$el.querySelector('.time-tracking-estimate-only-pane').innerText; + const correctText = 'Estimated: 2h 46m'; + + expect(estimateText).toBe(correctText); + done(); + }); + }); + }); + + describe('Spent only pane', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 5000, timeEstimateHumanReadable: '2h 46m', timeSpentHumanReadable: '1h 23m' }); + }); + + it('should display the human readable version of time spent', function(done) { + Vue.nextTick(() => { + const spentText = this.timeTracker.$el.querySelector('.time-tracking-spend-only-pane').innerText; + const correctText = 'Spent: 1h 23m'; + + expect(spentText).toBe(correctText); + done(); + }); + }); + }); + + describe('No time tracking pane', function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0, timeEstimateHumanReadable: 0, timeSpentHumanReadable: 0 }); + }); + + it('should only show the "No time tracking" pane when both timeEstimate and time_spent are falsey', function(done) { + Vue.nextTick(() => { + const $noTrackingPane = this.timeTracker.$el.querySelector('.time-tracking-no-tracking-pane'); + const noTrackingText =$noTrackingPane.innerText; + const correctText = 'No estimate or time spent'; + + expect(this.timeTracker.showNoTimeTrackingState).toBe(true); + expect($noTrackingPane).toBeVisible(); + expect(noTrackingText).toBe(correctText); + done(); + }); + }); + }); + + describe("Help pane", function() { + beforeEach(function() { + initTimeTrackingComponent.call(this, { timeEstimate: 0, timeSpent: 0 }); + }); + + it('should not show the "Help" pane by default', function(done) { + Vue.nextTick(() => { + const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); + + expect(this.timeTracker.showHelpState).toBe(false); + expect($helpPane).toBeNull(); + done(); + }); + }); + + it('should show the "Help" pane when help button is clicked', function(done) { + Vue.nextTick(() => { + $(this.timeTracker.$el).find('.help-button').click(); + + setTimeout(() => { + const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); + expect(this.timeTracker.showHelpState).toBe(true); + expect($helpPane).toBeVisible(); + done(); + }, 10); + }); + }); + + it('should not show the "Help" pane when help button is clicked and then closed', function(done) { + Vue.nextTick(() => { + $(this.timeTracker.$el).find('.help-button').click(); + + setTimeout(() => { + + $(this.timeTracker.$el).find('.close-help-button').click(); + + setTimeout(() => { + const $helpPane = this.timeTracker.$el.querySelector('.time-tracking-help-state'); + + expect(this.timeTracker.showHelpState).toBe(false); + expect($helpPane).toBeNull(); + + done(); + }, 1000); + }, 1000); + }); + }); + }); + }); + }); + }); +})(window.gl || (window.gl = {})); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index eb07421826c..673a4b3c07a 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ /* global Issue */ /*= require lib/utils/text_utility */ @@ -42,21 +42,21 @@ } function findElements() { - $boxClosed = $('div.status-box-closed'); - expect($boxClosed).toExist(); - expect($boxClosed).toHaveText('Closed'); + $boxClosed = $('div.status-box-closed'); + expect($boxClosed).toExist(); + expect($boxClosed).toHaveText('Closed'); - $boxOpen = $('div.status-box-open'); - expect($boxOpen).toExist(); - expect($boxOpen).toHaveText('Open'); + $boxOpen = $('div.status-box-open'); + expect($boxOpen).toExist(); + expect($boxOpen).toHaveText('Open'); - $btnClose = $('.btn-close.btn-grouped'); - expect($btnClose).toExist(); - expect($btnClose).toHaveText('Close issue'); + $btnClose = $('.btn-close.btn-grouped'); + expect($btnClose).toExist(); + expect($btnClose).toHaveText('Close issue'); - $btnReopen = $('.btn-reopen.btn-grouped'); - expect($btnReopen).toExist(); - expect($btnReopen).toHaveText('Reopen issue'); + $btnReopen = $('.btn-reopen.btn-grouped'); + expect($btnReopen).toExist(); + expect($btnReopen).toHaveText('Reopen issue'); } describe('Issue', function() { @@ -161,5 +161,4 @@ expect($btnReopen).toHaveProp('disabled', false); }); }); - }).call(this); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index e3146559a4a..0d19b4a25b9 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-new, no-plusplus, object-curly-spacing, prefer-const, semi */ +/* eslint-disable no-new */ /* global IssuableContext */ /* global LabelsSelect */ @@ -26,18 +26,18 @@ spyOn(jQuery, 'ajax').and.callFake((req) => { const d = $.Deferred(); - let LABELS_DATA = [] + let LABELS_DATA = []; if (req.url === '/root/test/labels.json') { - for (let i = 0; i < 10; i++) { - LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'}); + for (let i = 0; i < 10; i += 1) { + LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' }); } } else if (req.url === '/root/test/issues/2.json') { - let tmp = [] - for (let i = 0; i < saveLabelCount; i++) { - tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'}); + const tmp = []; + for (let i = 0; i < saveLabelCount; i += 1) { + tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' }); } - LABELS_DATA = {labels: tmp}; + LABELS_DATA = { labels: tmp }; } d.resolve(LABELS_DATA); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index 31f516b41bf..6605986c33a 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */ /* global LineHighlighter */ /*= require line_highlighter */ @@ -33,11 +33,11 @@ return expect($('#LC13')).toHaveClass(this.css); }); it('highlights a range of lines given in the URL hash', function() { - var i, line, results; + var line, results; new LineHighlighter('#L5-25'); expect($("." + this.css).length).toBe(21); results = []; - for (line = i = 5; i <= 25; line = ++i) { + for (line = 5; line <= 25; line += 1) { results.push(expect($("#LC" + line)).toHaveClass(this.css)); } return results; @@ -124,27 +124,27 @@ }); describe('with existing single-line highlight', function() { it('uses existing line as last line when target is lesser', function() { - var i, line, results; + var line, results; clickLine(20); clickLine(15, { shiftKey: true }); expect($("." + this.css).length).toBe(6); results = []; - for (line = i = 15; i <= 20; line = ++i) { + for (line = 15; line <= 20; line += 1) { results.push(expect($("#LC" + line)).toHaveClass(this.css)); } return results; }); return it('uses existing line as first line when target is greater', function() { - var i, line, results; + var line, results; clickLine(5); clickLine(10, { shiftKey: true }); expect($("." + this.css).length).toBe(6); results = []; - for (line = i = 5; i <= 10; line = ++i) { + for (line = 5; line <= 10; line += 1) { results.push(expect($("#LC" + line)).toHaveClass(this.css)); } return results; @@ -160,25 +160,25 @@ }); }); it('uses target as first line when it is less than existing first line', function() { - var i, line, results; + var line, results; clickLine(5, { shiftKey: true }); expect($("." + this.css).length).toBe(6); results = []; - for (line = i = 5; i <= 10; line = ++i) { + for (line = 5; line <= 10; line += 1) { results.push(expect($("#LC" + line)).toHaveClass(this.css)); } return results; }); return it('uses target as last line when it is greater than existing first line', function() { - var i, line, results; + var line, results; clickLine(15, { shiftKey: true }); expect($("." + this.css).length).toBe(6); results = []; - for (line = i = 10; i <= 15; line = ++i) { + for (line = 10; line <= 15; line += 1) { results.push(expect($("#LC" + line)).toHaveClass(this.css)); } return results; @@ -227,5 +227,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 9b232617fe5..f644d39b1c7 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */ +/* eslint-disable space-before-function-paren, no-return-assign */ /* global MergeRequest */ /*= require merge_request */ @@ -26,5 +26,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 6f91529db00..bf45100af03 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */ /*= require merge_request_widget */ /*= require lib/utils/datetime_utility */ @@ -42,17 +42,17 @@ }); it('should call renderEnvironments when the environments property is set', function() { - const spy = spyOn(this.class, 'renderEnvironments').and.stub(); - this.class.getCIEnvironmentsStatus(); - expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData); - }); - - it('should not call renderEnvironments when the environments property is not set', function() { - this.ciEnvironmentsStatusData = null; - const spy = spyOn(this.class, 'renderEnvironments').and.stub(); - this.class.getCIEnvironmentsStatus(); - expect(spy).not.toHaveBeenCalled(); - }); + const spy = spyOn(this.class, 'renderEnvironments').and.stub(); + this.class.getCIEnvironmentsStatus(); + expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData); + }); + + it('should not call renderEnvironments when the environments property is not set', function() { + this.ciEnvironmentsStatusData = null; + const spy = spyOn(this.class, 'renderEnvironments').and.stub(); + this.class.getCIEnvironmentsStatus(); + expect(spy).not.toHaveBeenCalled(); + }); }); describe('renderEnvironments', function() { @@ -107,16 +107,16 @@ }); describe('mergeInProgress', function() { - it('should display error with h4 tag', function() { - spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) { - expect(html).toBe('<h4>Sorry, something went wrong.</h4>'); - }); - spyOn($, 'ajax').and.callFake(function(e) { - e.success({ merge_error: 'Sorry, something went wrong.' }); - }); - this.class.mergeInProgress(null); + it('should display error with h4 tag', function() { + spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) { + expect(html).toBe('<h4>Sorry, something went wrong.</h4>'); + }); + spyOn($, 'ajax').and.callFake(function(e) { + e.success({ merge_error: 'Sorry, something went wrong.' }); }); + this.class.mergeInProgress(null); }); + }); return describe('getCIStatus', function() { beforeEach(function() { @@ -167,5 +167,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index e0dc549a9f4..8259d553f1b 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ /* global NewBranchForm */ /*= require jquery-ui/autocomplete */ @@ -166,5 +166,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 9cdb0a5d5aa..015c35dfca7 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ /* global Notes */ /*= require notes */ @@ -72,8 +72,7 @@ $('.js-comment-button').click(); expect(this.autoSizeSpy).toHaveBeenTriggered(); - }) + }); }); }); - }).call(this); diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6 index 2e12d45f7a7..7a04fba5f7f 100644 --- a/spec/javascripts/pretty_time_spec.js.es6 +++ b/spec/javascripts/pretty_time_spec.js.es6 @@ -1,12 +1,12 @@ //= require lib/utils/pretty_time (() => { - const PrettyTime = gl.PrettyTime; + const prettyTime = gl.utils.prettyTime; - describe('PrettyTime methods', function () { + describe('prettyTime methods', function () { describe('parseSeconds', function () { it('should correctly parse a negative value', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const zeroSeconds = parser(-1000); @@ -17,7 +17,7 @@ }); it('should correctly parse a zero value', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const zeroSeconds = parser(0); @@ -28,7 +28,7 @@ }); it('should correctly parse a small non-zero second values', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const subOneMinute = parser(10); @@ -53,7 +53,7 @@ }); it('should correctly parse large second values', function () { - const parser = PrettyTime.parseSeconds; + const parser = prettyTime.parseSeconds; const aboveOneHour = parser(4800); @@ -87,7 +87,7 @@ minutes: 20, }; - const timeString = PrettyTime.stringifyTime(timeObject); + const timeString = prettyTime.stringifyTime(timeObject); expect(timeString).toBe('1w 4d 7h 20m'); }); @@ -100,7 +100,7 @@ minutes: 20, }; - const timeString = PrettyTime.stringifyTime(timeObject); + const timeString = prettyTime.stringifyTime(timeObject); expect(timeString).toBe('4d 20m'); }); @@ -113,7 +113,7 @@ minutes: 0, }; - const timeString = PrettyTime.stringifyTime(timeObject); + const timeString = prettyTime.stringifyTime(timeObject); expect(timeString).toBe('0m'); }); @@ -122,12 +122,12 @@ describe('abbreviateTime', function () { it('should abbreviate stringified times for weeks', function () { const fullTimeString = '1w 3d 4h 5m'; - expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('1w'); + expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w'); }); it('should abbreviate stringified times for non-weeks', function () { const fullTimeString = '0w 3d 4h 5m'; - expect(PrettyTime.abbreviateTime(fullTimeString)).toBe('3d'); + expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d'); }); }); }); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 27b071f266d..0202c9ba85e 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, max-len */ /* global Project */ @@ -49,5 +49,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 0177d8e4e79..942778229b5 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */ /* global Sidebar */ /*= require right_sidebar */ @@ -78,7 +78,6 @@ $('.js-issuable-todo').click(); expect(todoToggleSpy.calls.count()).toEqual(1); - }) + }); }); - }).call(this); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 2d3f44e7980..7ac9710654f 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */ /*= require gl_dropdown */ /*= require search_autocomplete */ @@ -171,5 +171,4 @@ expect(enterKeyEvent.isDefaultPrevented()).toBe(true); }); }); - }).call(this); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index c2894d6f3ea..386fc8f514e 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */ +/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */ /* global ShortcutsIssuable */ /*= require copy_as_gfm */ @@ -78,5 +78,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index 6a70dd856a7..99f45850ea3 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */ +/* eslint-disable max-len, arrow-parens, comma-dangle */ //= vue //= vue-resource @@ -53,7 +53,7 @@ this.MockResource.subscribe(callbacks.two); this.MockResource.subscribe(callbacks.three); - state.myprop++; + state.myprop += 1; this.MockResource.publish(state); diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index 5984ce8ffd4..436f7064a69 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes, padded-blocks */ +/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */ /*= require syntax_highlight */ @@ -41,5 +41,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index dc2f4967985..80163fd72d3 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, max-len */ /* global MockU2FDevice */ /* global U2FAuthenticate */ @@ -69,5 +69,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js index 1459f968c3d..287bfb4138b 100644 --- a/spec/javascripts/u2f/mock_u2f_device.js +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -1,6 +1,7 @@ -/* eslint-disable space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, no-param-reassign, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-expressions, no-return-assign, 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); }; }; this.MockU2FDevice = (function() { function MockU2FDevice() { @@ -28,7 +29,5 @@ }; return MockU2FDevice; - })(); - }).call(this); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index ab4c5edd044..0790553b67e 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, max-len */ /* global MockU2FDevice */ /* global U2FRegister */ @@ -74,5 +74,4 @@ }); }); }); - }).call(this); diff --git a/spec/javascripts/visibility_select_spec.js.es6 b/spec/javascripts/visibility_select_spec.js.es6 new file mode 100644 index 00000000000..b21f6912e06 --- /dev/null +++ b/spec/javascripts/visibility_select_spec.js.es6 @@ -0,0 +1,100 @@ +/*= require visibility_select */ + +(() => { + const VisibilitySelect = gl.VisibilitySelect; + + describe('VisibilitySelect', function () { + const lockedElement = document.createElement('div'); + lockedElement.dataset.helpBlock = 'lockedHelpBlock'; + + const checkedElement = document.createElement('div'); + checkedElement.dataset.description = 'checkedDescription'; + + const mockElements = { + container: document.createElement('div'), + select: document.createElement('div'), + '.help-block': document.createElement('div'), + '.js-locked': lockedElement, + 'option:checked': checkedElement, + }; + + beforeEach(function () { + spyOn(Element.prototype, 'querySelector').and.callFake(selector => mockElements[selector]); + }); + + describe('#constructor', function () { + beforeEach(function () { + this.visibilitySelect = new VisibilitySelect(mockElements.container); + }); + + it('sets the container member', function () { + expect(this.visibilitySelect.container).toEqual(mockElements.container); + }); + + it('queries and sets the helpBlock member', function () { + expect(Element.prototype.querySelector).toHaveBeenCalledWith('.help-block'); + expect(this.visibilitySelect.helpBlock).toEqual(mockElements['.help-block']); + }); + + it('queries and sets the select member', function () { + expect(Element.prototype.querySelector).toHaveBeenCalledWith('select'); + expect(this.visibilitySelect.select).toEqual(mockElements.select); + }); + + describe('if there is no container element provided', function () { + it('throws an error', function () { + expect(() => new VisibilitySelect()).toThrowError('VisibilitySelect requires a container element as argument 1'); + }); + }); + }); + + describe('#init', function () { + describe('if there is a select', function () { + beforeEach(function () { + this.visibilitySelect = new VisibilitySelect(mockElements.container); + }); + + it('calls updateHelpText', function () { + spyOn(VisibilitySelect.prototype, 'updateHelpText'); + this.visibilitySelect.init(); + expect(this.visibilitySelect.updateHelpText).toHaveBeenCalled(); + }); + + it('adds a change event listener', function () { + spyOn(this.visibilitySelect.select, 'addEventListener'); + this.visibilitySelect.init(); + expect(this.visibilitySelect.select.addEventListener.calls.argsFor(0)).toContain('change'); + }); + }); + + describe('if there is no select', function () { + beforeEach(function () { + mockElements.select = undefined; + this.visibilitySelect = new VisibilitySelect(mockElements.container); + this.visibilitySelect.init(); + }); + + it('updates the helpBlock text to the locked `data-help-block` messaged', function () { + expect(this.visibilitySelect.helpBlock.textContent) + .toEqual(lockedElement.dataset.helpBlock); + }); + + afterEach(function () { + mockElements.select = document.createElement('div'); + }); + }); + }); + + describe('#updateHelpText', function () { + beforeEach(function () { + this.visibilitySelect = new VisibilitySelect(mockElements.container); + this.visibilitySelect.init(); + }); + + it('updates the helpBlock text to the selected options `data-description`', function () { + expect(this.visibilitySelect.helpBlock.textContent) + .toEqual(checkedElement.dataset.description); + }); + }); + }); +})(); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index f1c2edcc55c..be706ca304f 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, max-len */ /* global Dropzone */ /* global Mousetrap */ /* global ZenMode */ @@ -76,5 +76,4 @@ keyCode: 27 })); }; - }).call(this); diff --git a/spec/lib/gitlab/ci/status/external/common_spec.rb b/spec/lib/gitlab/ci/status/external/common_spec.rb new file mode 100644 index 00000000000..5a97d98b55f --- /dev/null +++ b/spec/lib/gitlab/ci/status/external/common_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::External::Common do + let(:user) { create(:user) } + let(:project) { external_status.project } + let(:external_target_url) { 'http://example.gitlab.com/status' } + + let(:external_status) do + create(:generic_commit_status, target_url: external_target_url) + end + + subject do + Gitlab::Ci::Status::Core + .new(external_status, user) + .extend(described_class) + end + + describe '#has_action?' do + it { is_expected.not_to have_action } + end + + describe '#has_details?' do + context 'when user has access to read commit status' do + before { project.team << [user, :developer] } + + it { is_expected.to have_details } + end + + context 'when user does not have access to read commit status' do + it { is_expected.not_to have_details } + end + end + + describe '#details_path' do + it 'links to the external target URL' do + expect(subject.details_path).to eq external_target_url + end + end +end diff --git a/spec/lib/gitlab/ci/status/external/factory_spec.rb b/spec/lib/gitlab/ci/status/external/factory_spec.rb new file mode 100644 index 00000000000..c96fd53e730 --- /dev/null +++ b/spec/lib/gitlab/ci/status/external/factory_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::External::Factory do + let(:user) { create(:user) } + let(:project) { resource.project } + let(:status) { factory.fabricate! } + let(:factory) { described_class.new(resource, user) } + let(:external_url) { 'http://gitlab.com/status' } + + before do + project.team << [user, :developer] + end + + context 'when external status has a simple core status' do + HasStatus::AVAILABLE_STATUSES.each do |simple_status| + context "when core status is #{simple_status}" do + let(:resource) do + create(:generic_commit_status, status: simple_status, + target_url: external_url) + end + + let(:expected_status) do + Gitlab::Ci::Status.const_get(simple_status.capitalize) + end + + it "fabricates a core status #{simple_status}" do + expect(status).to be_a expected_status + end + + it 'extends core status with common methods' do + expect(status).to have_details + expect(status).not_to have_action + expect(status.details_path).to eq external_url + end + end + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb new file mode 100644 index 00000000000..0267e8c2f69 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::CodeEventFetcher do + let(:stage_name) { :code } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb deleted file mode 100644 index 43f42d1bde8..00000000000 --- a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::CodeEvent do - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb new file mode 100644 index 00000000000..e8fc67acf05 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::CodeStage do + let(:stage_name) { :code } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 6062e7af4f5..9d2ba481919 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' -describe Gitlab::CycleAnalytics::Events do +describe 'cycle analytics events' do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } - subject { described_class.new(project: project, options: { from: from_date, current_user: user }) } + let(:events) do + CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events + end before do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context]) @@ -15,104 +17,112 @@ describe Gitlab::CycleAnalytics::Events do end describe '#issue_events' do + let(:stage) { :issue } + it 'has the total time' do - expect(subject.issue_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.issue_events.first[:title]).to eq(context.title) + expect(events.first[:title]).to eq(context.title) end it 'has the URL' do - expect(subject.issue_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has an iid' do - expect(subject.issue_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.issue_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.issue_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.issue_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.issue_events.first[:author][:name]).to eq(context.author.name) + expect(events.first[:author][:name]).to eq(context.author.name) end end describe '#plan_events' do + let(:stage) { :plan } + it 'has a title' do - expect(subject.plan_events.first[:title]).not_to be_nil + expect(events.first[:title]).not_to be_nil end it 'has a sha short ID' do - expect(subject.plan_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the URL' do - expect(subject.plan_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the total time' do - expect(subject.plan_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it "has the author's URL" do - expect(subject.plan_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.plan_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.plan_events.first[:author][:name]).not_to be_nil + expect(events.first[:author][:name]).not_to be_nil end end describe '#code_events' do + let(:stage) { :code } + before do create_commit_referencing_issue(context) end it 'has the total time' do - expect(subject.code_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.code_events.first[:title]).to eq('Awesome merge_request') + expect(events.first[:title]).to eq('Awesome merge_request') end it 'has an iid' do - expect(subject.code_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.code_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.code_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.code_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.code_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#test_events' do + let(:stage) { :test } + let(:merge_request) { MergeRequest.first } let!(:pipeline) do create(:ci_pipeline, @@ -130,83 +140,85 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the name' do - expect(subject.test_events.first[:name]).not_to be_nil + expect(events.first[:name]).not_to be_nil end it 'has the ID' do - expect(subject.test_events.first[:id]).not_to be_nil + expect(events.first[:id]).not_to be_nil end it 'has the URL' do - expect(subject.test_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has the branch name' do - expect(subject.test_events.first[:branch]).not_to be_nil + expect(events.first[:branch]).not_to be_nil end it 'has the branch URL' do - expect(subject.test_events.first[:branch][:url]).not_to be_nil + expect(events.first[:branch][:url]).not_to be_nil end it 'has the short SHA' do - expect(subject.test_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the commit URL' do - expect(subject.test_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the date' do - expect(subject.test_events.first[:date]).not_to be_nil + expect(events.first[:date]).not_to be_nil end it 'has the total time' do - expect(subject.test_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end end describe '#review_events' do + let(:stage) { :review } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } it 'has the total time' do - expect(subject.review_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.review_events.first[:title]).to eq('Awesome merge_request') + expect(events.first[:title]).to eq('Awesome merge_request') end it 'has an iid' do - expect(subject.review_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has the URL' do - expect(subject.review_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has a state' do - expect(subject.review_events.first[:state]).not_to be_nil + expect(events.first[:state]).not_to be_nil end it 'has a created_at timestamp' do - expect(subject.review_events.first[:created_at]).not_to be_nil + expect(events.first[:created_at]).not_to be_nil end it "has the author's URL" do - expect(subject.review_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.review_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.review_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#staging_events' do + let(:stage) { :staging } let(:merge_request) { MergeRequest.first } let!(:pipeline) do create(:ci_pipeline, @@ -227,55 +239,56 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the name' do - expect(subject.staging_events.first[:name]).not_to be_nil + expect(events.first[:name]).not_to be_nil end it 'has the ID' do - expect(subject.staging_events.first[:id]).not_to be_nil + expect(events.first[:id]).not_to be_nil end it 'has the URL' do - expect(subject.staging_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has the branch name' do - expect(subject.staging_events.first[:branch]).not_to be_nil + expect(events.first[:branch]).not_to be_nil end it 'has the branch URL' do - expect(subject.staging_events.first[:branch][:url]).not_to be_nil + expect(events.first[:branch][:url]).not_to be_nil end it 'has the short SHA' do - expect(subject.staging_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the commit URL' do - expect(subject.staging_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the date' do - expect(subject.staging_events.first[:date]).not_to be_nil + expect(events.first[:date]).not_to be_nil end it 'has the total time' do - expect(subject.staging_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it "has the author's URL" do - expect(subject.staging_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.staging_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.staging_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#production_events' do + let(:stage) { :production } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } before do @@ -284,35 +297,35 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the total time' do - expect(subject.production_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.production_events.first[:title]).to eq(context.title) + expect(events.first[:title]).to eq(context.title) end it 'has the URL' do - expect(subject.production_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has an iid' do - expect(subject.production_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.production_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.production_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.production_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.production_events.first[:author][:name]).to eq(context.author.name) + expect(events.first[:author][:name]).to eq(context.author.name) end end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb new file mode 100644 index 00000000000..fd9fa2fee49 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::IssueEventFetcher do + let(:stage_name) { :issue } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb deleted file mode 100644 index 1c5c308da7d..00000000000 --- a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::IssueEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb new file mode 100644 index 00000000000..3127f01989d --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::IssueStage do + let(:stage_name) { :issue } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb index 4a5604115ec..2e5dc5b5547 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb @@ -1,15 +1,13 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' -describe Gitlab::CycleAnalytics::PlanEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end +describe Gitlab::CycleAnalytics::PlanEventFetcher do + let(:stage_name) { :plan } + it_behaves_like 'default query config' do context 'no commits' do it 'does not blow up if there are no commits' do - allow_any_instance_of(Gitlab::CycleAnalytics::EventsQuery).to receive(:execute).and_return([{}]) + allow(event).to receive(:event_result).and_return([{}]) expect { event.fetch }.not_to raise_error end diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb new file mode 100644 index 00000000000..4c715921ad6 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::PlanStage do + let(:stage_name) { :plan } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb new file mode 100644 index 00000000000..74001181305 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::ProductionEventFetcher do + let(:stage_name) { :production } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb deleted file mode 100644 index ac17e3b4287..00000000000 --- a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::ProductionEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb new file mode 100644 index 00000000000..916684b81eb --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::ProductionStage do + let(:stage_name) { :production } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb new file mode 100644 index 00000000000..4f67c95ed4c --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::ReviewEventFetcher do + let(:stage_name) { :review } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb deleted file mode 100644 index 1ff53aa0227..00000000000 --- a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::ReviewEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb new file mode 100644 index 00000000000..1412c8dfa08 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::ReviewStage do + let(:stage_name) { :review } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index 7019e4c3351..9c5e57342e9 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -1,20 +1,13 @@ require 'spec_helper' shared_examples 'default query config' do - let(:event) { described_class.new(project: double, options: {}) } - - it 'has the start attributes' do - expect(event.start_time_attrs).not_to be_nil - end + let(:project) { create(:empty_project) } + let(:event) { described_class.new(project: project, stage: stage_name, options: { from: 1.day.ago }) } it 'has the stage attribute' do expect(event.stage).not_to be_nil end - it 'has the end attributes' do - expect(event.end_time_attrs).not_to be_nil - end - it 'has the projection attributes' do expect(event.projections).not_to be_nil end diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb new file mode 100644 index 00000000000..08425acbfc8 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +shared_examples 'base stage' do + let(:stage) { described_class.new(project: double, options: {}) } + + before do + allow(stage).to receive(:median).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) + end + + it 'has the median data value' do + expect(stage.as_json[:value]).not_to be_nil + end + + it 'has the median data stage' do + expect(stage.as_json[:title]).not_to be_nil + end + + it 'has the median data description' do + expect(stage.as_json[:description]).not_to be_nil + end + + it 'has the title' do + expect(stage.title).to eq(stage_name.to_s.capitalize) + end + + it 'has the events' do + expect(stage.events).not_to be_nil + end +end diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb index 725bc68b25f..fb6b6c4a8d2 100644 --- a/spec/models/cycle_analytics/summary_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -1,23 +1,23 @@ require 'spec_helper' -describe CycleAnalytics::Summary, models: true do +describe Gitlab::CycleAnalytics::StageSummary, models: true do let(:project) { create(:project) } - let(:from) { Time.now } + let(:from) { 1.day.ago } let(:user) { create(:user, :admin) } - subject { described_class.new(project, user, from: from) } + subject { described_class.new(project, from: Time.now, current_user: user).data } describe "#new_issues" do it "finds the number of issues created after the 'from date'" do Timecop.freeze(5.days.ago) { create(:issue, project: project) } Timecop.freeze(5.days.from_now) { create(:issue, project: project) } - expect(subject.new_issues).to eq(1) + expect(subject.first[:value]).to eq(1) end it "doesn't find issues from other projects" do Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) } - expect(subject.new_issues).to eq(0) + expect(subject.first[:value]).to eq(0) end end @@ -26,19 +26,19 @@ describe CycleAnalytics::Summary, models: true do Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') } Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') } - expect(subject.commits).to eq(1) + expect(subject.second[:value]).to eq(1) end it "doesn't find commits from other projects" do Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') } - expect(subject.commits).to eq(0) + expect(subject.second[:value]).to eq(0) end it "finds a large (> 100) snumber of commits if present" do Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) } - expect(subject.commits).to eq(100) + expect(subject.second[:value]).to eq(100) end end @@ -47,13 +47,13 @@ describe CycleAnalytics::Summary, models: true do Timecop.freeze(5.days.ago) { create(:deployment, project: project) } Timecop.freeze(5.days.from_now) { create(:deployment, project: project) } - expect(subject.deploys).to eq(1) + expect(subject.third[:value]).to eq(1) end it "doesn't find commits from other projects" do Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) } - expect(subject.deploys).to eq(0) + expect(subject.third[:value]).to eq(0) end end end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb new file mode 100644 index 00000000000..bbc82496340 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::StagingEventFetcher do + let(:stage_name) { :staging } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb deleted file mode 100644 index 4862d4765f2..00000000000 --- a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::StagingEvent do - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb new file mode 100644 index 00000000000..8154b3ac701 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::StagingStage do + let(:stage_name) { :staging } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb new file mode 100644 index 00000000000..6639fa54e0e --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::TestEventFetcher do + let(:stage_name) { :test } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb deleted file mode 100644 index e249db69fc6..00000000000 --- a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::TestEvent do - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb new file mode 100644 index 00000000000..eacde22cd56 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::TestStage do + let(:stage_name) { :test } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index ceed9c942c1..7fb6829f582 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -15,6 +15,7 @@ issues: - events - merge_requests_closing_issues - metrics +- timelogs events: - author - project @@ -77,6 +78,7 @@ merge_requests: - events - merge_requests_closing_issues - metrics +- timelogs merge_request_diff: - merge_request pipelines: @@ -198,3 +200,6 @@ award_emoji: - user priorities: - label +timelogs: +- trackable +- user diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index d88a141b458..493bc2db21a 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -20,6 +20,7 @@ Issue: - lock_version - milestone_id - weight +- time_estimate Event: - id - target_type @@ -150,6 +151,7 @@ MergeRequest: - milestone_id - approvals_before_merge - rebase_commit_sha +- time_estimate MergeRequestDiff: - id - state @@ -344,3 +346,11 @@ LabelPriority: - priority - created_at - updated_at +Timelog: +- id +- time_spent +- trackable_id +- trackable_type +- user_id +- created_at +- updated_at diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb new file mode 100644 index 00000000000..f2c152cdcd4 --- /dev/null +++ b/spec/lib/gitlab/view/presenter/base_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Gitlab::View::Presenter::Base do + let(:project) { double(:project) } + let(:presenter_class) do + Struct.new(:subject).include(described_class) + end + + describe '.presenter?' do + it 'returns true' do + presenter = presenter_class.new(project) + + expect(presenter.class).to be_presenter + end + end + + describe '.presents' do + it 'exposes #subject with the given keyword' do + presenter_class.presents(:foo) + presenter = presenter_class.new(project) + + expect(presenter.foo).to eq(project) + end + end + + describe '#can?' do + context 'user is not allowed' do + it 'returns false' do + presenter = presenter_class.new(build_stubbed(:empty_project)) + + expect(presenter.can?(nil, :read_project)).to be_falsy + end + end + + context 'user is allowed' do + it 'returns true' do + presenter = presenter_class.new(build_stubbed(:empty_project, :public)) + + expect(presenter.can?(nil, :read_project)).to be_truthy + end + end + + context 'subject is overriden' do + it 'returns true' do + presenter = presenter_class.new(build_stubbed(:empty_project, :public)) + + expect(presenter.can?(nil, :read_project, build_stubbed(:empty_project))).to be_falsy + end + end + end +end diff --git a/spec/lib/gitlab/view/presenter/delegated_spec.rb b/spec/lib/gitlab/view/presenter/delegated_spec.rb new file mode 100644 index 00000000000..888ab80cad5 --- /dev/null +++ b/spec/lib/gitlab/view/presenter/delegated_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::View::Presenter::Delegated do + let(:project) { double(:project, bar: 'baz') } + let(:presenter_class) do + Class.new(described_class) + end + + it 'includes Gitlab::View::Presenter::Base' do + expect(described_class).to include(Gitlab::View::Presenter::Base) + end + + describe '#initialize' do + it 'takes arbitrary key/values and exposes them' do + presenter = presenter_class.new(project, user: 'user', foo: 'bar') + + expect(presenter.user).to eq('user') + expect(presenter.foo).to eq('bar') + end + end + + describe 'delegation' do + it 'forwards missing methods to subject' do + presenter = presenter_class.new(project) + + expect(presenter.bar).to eq('baz') + end + end +end diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb new file mode 100644 index 00000000000..55c5ecbf92f --- /dev/null +++ b/spec/lib/gitlab/view/presenter/factory_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::View::Presenter::Factory do + let(:build) { Ci::Build.new } + + describe '#initialize' do + context 'without optional parameters' do + it 'takes a subject and optional params' do + presenter = described_class.new(build) + + expect { presenter }.not_to raise_error + end + end + + context 'with optional parameters' do + it 'takes a subject and optional params' do + presenter = described_class.new(build, user: 'user') + + expect { presenter }.not_to raise_error + end + end + end + + describe '#fabricate!' do + it 'exposes given params' do + presenter = described_class.new(build, user: 'user', foo: 'bar').fabricate! + + expect(presenter.user).to eq('user') + expect(presenter.foo).to eq('bar') + end + + it 'detects the presenter based on the given subject' do + presenter = described_class.new(build).fabricate! + + expect(presenter).to be_a(Ci::BuildPresenter) + end + end +end diff --git a/spec/lib/gitlab/view/presenter/simple_spec.rb b/spec/lib/gitlab/view/presenter/simple_spec.rb new file mode 100644 index 00000000000..b489bdf1981 --- /dev/null +++ b/spec/lib/gitlab/view/presenter/simple_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::View::Presenter::Simple do + let(:project) { double(:project) } + let(:presenter_class) do + Class.new(described_class) + end + + it 'includes Gitlab::View::Presenter::Base' do + expect(described_class).to include(Gitlab::View::Presenter::Base) + end + + describe '#initialize' do + it 'takes arbitrary key/values and exposes them' do + presenter = presenter_class.new(project, user: 'user', foo: 'bar') + + expect(presenter.user).to eq('user') + expect(presenter.foo).to eq('bar') + end + end + + describe 'delegation' do + it 'does not forward missing methods to subject' do + presenter = presenter_class.new(project) + + expect { presenter.foo }.to raise_error(NoMethodError) + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index af0f6a31eda..3309a7fff9f 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1013,6 +1013,24 @@ describe Ci::Build, :models do end end + describe '#has_expiring_artifacts?' do + context 'when artifacts have expiration date set' do + before { build.update(artifacts_expire_at: 1.day.from_now) } + + it 'has expiring artifacts' do + expect(build).to have_expiring_artifacts + end + end + + context 'when artifacts do not have expiration date set' do + before { build.update(artifacts_expire_at: nil) } + + it 'does not have expiring artifacts' do + expect(build).not_to have_expiring_artifacts + end + end + end + describe '#has_trace_file?' do context 'when there is no trace' do it { expect(build.has_trace_file?).to be_falsey } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 74b50d2908d..0d425ab7fd4 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -323,4 +323,32 @@ eos expect(new_commit.message).to eq(commit.message) end end + + describe '#work_in_progress?' do + ['squash! ', 'fixup! ', 'wip: ', 'WIP: ', '[WIP] '].each do |wip_prefix| + it "detects the '#{wip_prefix}' prefix" do + commit.message = "#{wip_prefix}#{commit.message}" + + expect(commit).to be_work_in_progress + end + end + + it "detects WIP for a commit just saying 'wip'" do + commit.message = "wip" + + expect(commit).to be_work_in_progress + end + + it "doesn't detect WIP for a commit that begins with 'FIXUP! '" do + commit.message = "FIXUP! #{commit.message}" + + expect(commit).not_to be_work_in_progress + end + + it "doesn't detect WIP for words starting with WIP" do + commit.message = "Wipout #{commit.message}" + + expect(commit).not_to be_work_in_progress + end + end end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 1078c959419..d7d31892e12 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -408,4 +408,42 @@ describe Issue, "Issuable" do expect(issue.assignee_or_author?(user)).to eq(false) end end + + describe '#spend_time' do + let(:user) { create(:user) } + let(:issue) { create(:issue) } + + def spend_time(seconds) + issue.spend_time(duration: seconds, user: user) + issue.save! + end + + context 'adding time' do + it 'should update the total time spent' do + spend_time(1800) + + expect(issue.total_time_spent).to eq(1800) + end + end + + context 'substracting time' do + before do + spend_time(1800) + end + + it 'should update the total time spent' do + spend_time(-900) + + expect(issue.total_time_spent).to eq(900) + end + + context 'when time to substract exceeds the total time spent' do + it 'raise a validation error' do + expect do + spend_time(-3600) + end.to raise_error(ActiveRecord::RecordInvalid) + end + end + end + end end diff --git a/spec/models/concerns/presentable_spec.rb b/spec/models/concerns/presentable_spec.rb new file mode 100644 index 00000000000..941647a79fb --- /dev/null +++ b/spec/models/concerns/presentable_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Presentable do + let(:build) { Ci::Build.new } + + describe '#present' do + it 'returns a presenter' do + expect(build.present).to be_a(Ci::BuildPresenter) + end + + it 'takes optional attributes' do + expect(build.present(foo: 'bar').foo).to eq('bar') + end + end +end diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 7771785ead3..70f985afefb 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } context 'with deployment' do generate_cycle_analytics_spec( @@ -16,10 +16,10 @@ describe 'CycleAnalytics#code', feature: true do -> (context, data) do context.create_commit_referencing_issue(data[:issue]) end]], - end_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) - end]], + end_time_conditions: [["merge request that closes issue is created", + -> (context, data) do + context.create_merge_request_closing_issue(data[:issue]) + end]], post_fn: -> (context, data) do context.merge_merge_requests_closing_issue(data[:issue]) context.deploy_master @@ -37,7 +37,7 @@ describe 'CycleAnalytics#code', feature: true do deploy_master end - expect(subject.code).to be_nil + expect(subject[:code].median).to be_nil end end end @@ -50,10 +50,10 @@ describe 'CycleAnalytics#code', feature: true do -> (context, data) do context.create_commit_referencing_issue(data[:issue]) end]], - end_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) - end]], + end_time_conditions: [["merge request that closes issue is created", + -> (context, data) do + context.create_merge_request_closing_issue(data[:issue]) + end]], post_fn: -> (context, data) do context.merge_merge_requests_closing_issue(data[:issue]) end) @@ -69,7 +69,7 @@ describe 'CycleAnalytics#code', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.code).to be_nil + expect(subject[:code].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 5ed3d37f2fb..e4b6a8f4518 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :issue, @@ -42,7 +42,7 @@ describe 'CycleAnalytics#issue', models: true do merge_merge_requests_closing_issue(issue) end - expect(subject.issue).to be_nil + expect(subject[:issue].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb index baf3e3241a1..dc5b04852d6 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :plan, @@ -44,7 +44,7 @@ describe 'CycleAnalytics#plan', feature: true do create_merge_request_closing_issue(issue, source_branch: branch_name) merge_merge_requests_closing_issue(issue) - expect(subject.issue).to be_nil + expect(subject[:issue].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 21b9c6e7150..5e99188f318 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :production, @@ -35,7 +35,7 @@ describe 'CycleAnalytics#production', feature: true do deploy_master end - expect(subject.production).to be_nil + expect(subject[:production].median).to be_nil end end @@ -48,7 +48,7 @@ describe 'CycleAnalytics#production', feature: true do deploy_master(environment: 'staging') end - expect(subject.production).to be_nil + expect(subject[:production].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 158621d59a4..45baa5f7006 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :review, @@ -27,7 +27,7 @@ describe 'CycleAnalytics#review', feature: true do MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) end - expect(subject.review).to be_nil + expect(subject[:review].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index dad653964b7..77625aad580 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :staging, @@ -45,7 +45,7 @@ describe 'CycleAnalytics#staging', feature: true do deploy_master end - expect(subject.staging).to be_nil + expect(subject[:staging].median).to be_nil end end @@ -58,7 +58,7 @@ describe 'CycleAnalytics#staging', feature: true do deploy_master(environment: 'staging') end - expect(subject.staging).to be_nil + expect(subject[:staging].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index 2313724e8f3..27a117d2d76 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :test, @@ -35,7 +35,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -48,7 +48,7 @@ describe 'CycleAnalytics#test', feature: true do pipeline.succeed! end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -65,7 +65,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -82,7 +82,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end end diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index 6004bfdb7b7..f4c3e6d503f 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -1,10 +1,20 @@ require 'spec_helper' describe GenericCommitStatus, models: true do - let(:pipeline) { create(:ci_pipeline) } + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:external_url) { 'http://example.gitlab.com/status' } let(:generic_commit_status) do - create(:generic_commit_status, pipeline: pipeline) + create(:generic_commit_status, pipeline: pipeline, + target_url: external_url) + end + + describe 'validations' do + it { is_expected.to validate_length_of(:target_url).is_at_most(255) } + it { is_expected.to allow_value(nil).for(:target_url) } + it { is_expected.to allow_value('http://gitlab.com/s').for(:target_url) } + it { is_expected.not_to allow_value('javascript:alert(1)').for(:target_url) } end describe '#context' do @@ -22,10 +32,25 @@ describe GenericCommitStatus, models: true do describe '#detailed_status' do let(:user) { create(:user) } + let(:status) { generic_commit_status.detailed_status(user) } it 'returns detailed status object' do - expect(generic_commit_status.detailed_status(user)) - .to be_a Gitlab::Ci::Status::Success + expect(status).to be_a Gitlab::Ci::Status::Success + end + + context 'when user has ability to see datails' do + before { project.team << [user, :developer] } + + it 'details path points to an external URL' do + expect(status).to have_details + expect(status.details_path).to eq external_url + end + end + + context 'when user should not see details' do + it 'does not have details' do + expect(status).not_to have_details + end end end diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index eb876d105da..6d599e148a2 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -76,6 +76,32 @@ describe MergeRequestDiff, models: true do end end + describe '#save_diffs' do + it 'saves collected state' do + mr_diff = create(:merge_request).merge_request_diff + + expect(mr_diff.collected?).to be_truthy + end + + it 'saves overflow state' do + allow(Commit).to receive(:max_diff_options) + .and_return(max_lines: 0, max_files: 0) + + mr_diff = create(:merge_request).merge_request_diff + + expect(mr_diff.overflow?).to be_truthy + end + + it 'saves empty state' do + allow_any_instance_of(MergeRequestDiff).to receive(:commits) + .and_return([]) + + mr_diff = create(:merge_request).merge_request_diff + + expect(mr_diff.empty?).to be_truthy + end + end + describe '#commits_sha' do it 'returns all commits SHA using serialized commits' do subject.st_commits = [ diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 8d1385016fd..38c80ba53ad 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -773,10 +773,12 @@ describe MergeRequest, models: true do subject { create(:merge_request, source_project: project, merge_status: :unchecked) } context 'when it is not broken and has no conflicts' do - it 'is marked as mergeable' do + before do allow(subject).to receive(:broken?) { false } allow(project.repository).to receive(:can_be_merged?).and_return(true) + end + it 'is marked as mergeable' do expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged') end end @@ -787,6 +789,12 @@ describe MergeRequest, models: true do it 'becomes unmergeable' do expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged') end + + it 'creates Todo on unmergeability' do + expect_any_instance_of(TodoService).to receive(:merge_request_became_unmergeable).with(subject) + + subject.check_if_can_be_merged + end end context 'when it has conflicts' do @@ -1514,6 +1522,108 @@ describe MergeRequest, models: true do end end + describe '#mergeable_with_slash_command?' do + def create_pipeline(status) + create(:ci_pipeline_with_one_job, + project: project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha, + status: status) + end + + let(:project) { create(:project, :public, only_allow_merge_if_build_succeeds: true) } + let(:developer) { create(:user) } + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:mr_sha) { merge_request.diff_head_sha } + + before do + project.team << [developer, :developer] + end + + context 'when autocomplete_precheck is set to true' do + it 'is mergeable by developer' do + expect(merge_request.mergeable_with_slash_command?(developer, autocomplete_precheck: true)).to be_truthy + end + + it 'is not mergeable by normal user' do + expect(merge_request.mergeable_with_slash_command?(user, autocomplete_precheck: true)).to be_falsey + end + end + + context 'when autocomplete_precheck is set to false' do + it 'is mergeable by developer' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + end + + it 'is not mergeable by normal user' do + expect(merge_request.mergeable_with_slash_command?(user, last_diff_sha: mr_sha)).to be_falsey + end + + context 'closed MR' do + before do + merge_request.update_attribute(:state, :closed) + end + + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + end + end + + context 'MR with WIP' do + before do + merge_request.update_attribute(:title, 'WIP: some MR') + end + + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + end + end + + context 'sha differs from the MR diff_head_sha' do + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: 'some other sha')).to be_falsey + end + end + + context 'sha is not provided' do + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer)).to be_falsey + end + end + + context 'with pipeline ok' do + before do + create_pipeline(:success) + end + + it 'is mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + end + end + + context 'with failing pipeline' do + before do + create_pipeline(:failed) + end + + it 'is not mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey + end + end + + context 'with running pipeline' do + before do + create_pipeline(:running) + end + + it 'is mergeable' do + expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy + end + end + end + end + describe '#has_commits?' do before do allow(subject.merge_request_diff).to receive(:commits_count). diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index 77403cc9eb0..ff29f6f66ba 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -107,7 +107,7 @@ describe ProjectStatistics, models: true do describe '#update_repository_size' do before do - allow(project.repository).to receive(:size).and_return(12.megabytes) + allow(project.repository).to receive(:size).and_return(12) statistics.update_repository_size end diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb new file mode 100644 index 00000000000..f08935b6425 --- /dev/null +++ b/spec/models/timelog_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +RSpec.describe Timelog, type: :model do + subject { build(:timelog) } + + it { is_expected.to be_valid } + + it { is_expected.to validate_presence_of(:time_spent) } + it { is_expected.to validate_presence_of(:user) } +end diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb new file mode 100644 index 00000000000..63acc0b68cd --- /dev/null +++ b/spec/policies/base_policy_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe BasePolicy, models: true do + let(:build) { Ci::Build.new } + + describe '.class_for' do + it 'detects policy class based on the subject ancestors' do + expect(described_class.class_for(build)).to eq(Ci::BuildPolicy) + end + + it 'detects policy class for a presented subject' do + presentee = Ci::BuildPresenter.new(build) + + expect(described_class.class_for(presentee)).to eq(Ci::BuildPolicy) + end + end +end diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb new file mode 100644 index 00000000000..7a35da38b2b --- /dev/null +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Ci::BuildPresenter do + let(:project) { create(:empty_project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } + + subject(:presenter) do + described_class.new(build) + end + + it 'inherits from Gitlab::View::Presenter::Delegated' do + expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated) + end + + describe '#initialize' do + it 'takes a build and optional params' do + expect { presenter }.not_to raise_error + end + + it 'exposes build' do + expect(presenter.build).to eq(build) + end + + it 'forwards missing methods to build' do + expect(presenter.ref).to eq('master') + end + end + + describe '#erased_by_user?' do + it 'takes a build and optional params' do + expect(presenter).not_to be_erased_by_user + end + end + + describe '#erased_by_name' do + context 'when build is not erased' do + before do + expect(presenter).to receive(:erased_by_user?).and_return(false) + end + + it 'returns nil' do + expect(presenter.erased_by_name).to be_nil + end + end + + context 'when build is erased' do + before do + expect(presenter).to receive(:erased_by_user?).and_return(true) + expect(build).to receive(:erased_by). + and_return(double(:user, name: 'John Doe')) + end + + it 'returns the name of the eraser' do + expect(presenter.erased_by_name).to eq('John Doe') + end + end + end + + describe 'quack like a Ci::Build permission-wise' do + context 'user is not allowed' do + let(:project) { build_stubbed(:empty_project, public_builds: false) } + + it 'returns false' do + expect(presenter.can?(nil, :read_build)).to be_falsy + end + end + + context 'user is allowed' do + let(:project) { build_stubbed(:empty_project, :public) } + + it 'returns true' do + expect(presenter.can?(nil, :read_build)).to be_truthy + end + end + end +end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index 335efc4db6c..c1c7c0882de 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -152,8 +152,11 @@ describe API::CommitStatuses, api: true do context 'with all optional parameters' do before do - optional_params = { state: 'success', context: 'coverage', - ref: 'develop', target_url: 'url', description: 'test' } + optional_params = { state: 'success', + context: 'coverage', + ref: 'develop', + description: 'test', + target_url: 'http://gitlab.com/status' } post api(post_url, developer), optional_params end @@ -164,12 +167,12 @@ describe API::CommitStatuses, api: true do expect(json_response['status']).to eq('success') expect(json_response['name']).to eq('coverage') expect(json_response['ref']).to eq('develop') - expect(json_response['target_url']).to eq('url') expect(json_response['description']).to eq('test') + expect(json_response['target_url']).to eq('http://gitlab.com/status') end end - context 'invalid status' do + context 'when status is invalid' do before { post api(post_url, developer), state: 'invalid' } it 'does not create commit status' do @@ -177,7 +180,7 @@ describe API::CommitStatuses, api: true do end end - context 'request without state' do + context 'when request without a state made' do before { post api(post_url, developer) } it 'does not create commit status' do @@ -185,7 +188,7 @@ describe API::CommitStatuses, api: true do end end - context 'invalid commit' do + context 'when commit SHA is invalid' do let(:sha) { 'invalid_sha' } before { post api(post_url, developer), state: 'running' } @@ -193,6 +196,19 @@ describe API::CommitStatuses, api: true do expect(response).to have_http_status(404) end end + + context 'when target URL is an invalid address' do + before do + post api(post_url, developer), state: 'pending', + target_url: 'invalid url' + end + + it 'responds with bad request status and validation errors' do + expect(response).to have_http_status(400) + expect(json_response['message']['target_url']) + .to include 'must be a valid URL' + end + end end context 'reporter user' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 12dd4bd83f7..807c999b84a 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1193,4 +1193,10 @@ describe API::Issues, api: true do expect(response).to have_http_status(404) end end + + describe 'time tracking endpoints' do + let(:issuable) { issue } + + include_examples 'time tracking endpoints', 'issue' + end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index f032d1b683d..4e4fea1dad8 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -6,7 +6,7 @@ describe API::MergeRequests, api: true do let(:user) { create(:user) } let(:admin) { create(:user, :admin) } let(:non_member) { create(:user) } - let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } + let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) } let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } @@ -671,6 +671,12 @@ describe API::MergeRequests, api: true do end end + describe 'Time tracking' do + let(:issuable) { merge_request } + + include_examples 'time tracking endpoints', 'merge_request' + end + def mr_with_later_created_and_updated_at_time merge_request merge_request.created_at += 1.hour diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb new file mode 100644 index 00000000000..f9951826683 --- /dev/null +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe AnalyticsStageSerializer do + let(:serializer) do + described_class + .new.represent(resource) + end + + let(:json) { serializer.as_json } + let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) } + + before do + allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) + end + + it 'it generates payload for single object' do + expect(json).to be_kind_of Hash + end + + it 'contains important elements of AnalyticsStage' do + expect(json).to include(:title, :description, :value) + end +end diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb new file mode 100644 index 00000000000..7a84c8b0b40 --- /dev/null +++ b/spec/serializers/analytics_summary_serializer_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe AnalyticsSummarySerializer do + let(:serializer) do + described_class + .new.represent(resource) + end + + let(:json) { serializer.as_json } + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:resource) do + Gitlab::CycleAnalytics::Summary::Issue.new(project: double, + from: 1.day.ago, + current_user: user) + end + + before do + allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12) + end + + it 'it generates payload for single object' do + expect(json).to be_kind_of Hash + end + + it 'contains important elements of AnalyticsStage' do + expect(json).to include(:title, :value) + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 7e3705983fb..00d0e20f47c 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -237,6 +237,70 @@ describe MergeRequests::RefreshService, services: true do end end + context 'marking the merge request as work in progress' do + let(:refresh_service) { service.new(@project, @user) } + before do + allow(refresh_service).to receive(:execute_hooks) + end + + it 'marks the merge request as work in progress from fixup commits' do + fixup_merge_request = create(:merge_request, + source_project: @project, + source_branch: 'wip', + target_branch: 'master', + target_project: @project) + commits = fixup_merge_request.commits + oldrev = commits.last.id + newrev = commits.first.id + + refresh_service.execute(oldrev, newrev, 'refs/heads/wip') + fixup_merge_request.reload + + expect(fixup_merge_request.work_in_progress?).to eq(true) + expect(fixup_merge_request.notes.last.note).to match( + /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/ + ) + end + + it 'references the commit that caused the Work in Progress status' do + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + + allow(refresh_service).to receive(:find_new_commits) + refresh_service.instance_variable_set("@commits", [ + instance_double( + Commit, + id: 'aaaaaaa', + short_id: 'aaaaaaa', + title: 'Fix issue', + work_in_progress?: false + ), + instance_double( + Commit, + id: 'bbbbbbb', + short_id: 'bbbbbbb', + title: 'fixup! Fix issue', + work_in_progress?: true, + to_reference: 'bbbbbbb' + ), + instance_double( + Commit, + id: 'ccccccc', + short_id: 'ccccccc', + title: 'fixup! Fix issue', + work_in_progress?: true, + to_reference: 'ccccccc' + ), + ]) + + refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip') + reload_mrs + + expect(@merge_request.notes.last.note).to eq( + "marked as a **Work In Progress** from bbbbbbb" + ) + end + end + def reload_mrs @merge_request.reload @fork_merge_request.reload diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 88c786947d3..7d73c0ea5d0 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -121,6 +121,99 @@ describe MergeRequests::UpdateService, services: true do end end + context 'merge' do + let(:opts) do + { + merge: merge_request.diff_head_sha + } + end + + let(:service) { MergeRequests::UpdateService.new(project, user, opts) } + + context 'without pipeline' do + before do + merge_request.merge_error = 'Error' + + perform_enqueued_jobs do + service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request).to be_valid } + it { expect(@merge_request.state).to eq('merged') } + it { expect(@merge_request.merge_error).to be_nil } + end + + context 'with finished pipeline' do + before do + create(:ci_pipeline_with_one_job, + project: project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha, + status: :success) + + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request).to be_valid } + it { expect(@merge_request.state).to eq('merged') } + end + + context 'with active pipeline' do + before do + service_mock = double + create(:ci_pipeline_with_one_job, + project: project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + + expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user). + and_return(service_mock) + expect(service_mock).to receive(:execute).with(merge_request) + end + + it { service.execute(merge_request) } + end + + context 'with a non-authorised user' do + let(:visitor) { create(:user) } + let(:service) { MergeRequests::UpdateService.new(project, visitor, opts) } + + before do + merge_request.update_attribute(:merge_error, 'Error') + + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request.state).to eq('opened') } + it { expect(@merge_request.merge_error).not_to be_nil } + end + + context 'MR can not be merged when note sha != MR sha' do + let(:opts) do + { + merge: 'other_commit' + } + end + + before do + perform_enqueued_jobs do + @merge_request = service.execute(merge_request) + @merge_request = MergeRequest.find(merge_request.id) + end + end + + it { expect(@merge_request.state).to eq('opened') } + end + end + context 'todos' do let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 25804696d2e..b0cc3ce5f5a 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -63,6 +63,17 @@ describe Notes::CreateService, services: true do expect(note.note).to eq "HELLO\nWORLD" end end + + describe '/merge with sha option' do + let(:note_text) { %(HELLO\n/merge\nWORLD) } + let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') } + + it 'saves the note and exectues merge command' do + note = described_class.new(project, user, params).execute + + expect(note.note).to eq "HELLO\nWORLD" + end + end end end diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb index 960b5cd5e6f..1a64c8bbf00 100644 --- a/spec/services/notes/slash_commands_service_spec.rb +++ b/spec/services/notes/slash_commands_service_spec.rb @@ -86,6 +86,18 @@ describe Notes::SlashCommandsService, services: true do expect(note.noteable).to be_open end end + + describe '/spend' do + let(:note_text) { '/spend 1h' } + + it 'updates the spent time on the noteable' do + content, command_params = service.extract_commands(note) + service.execute(command_params, note) + + expect(content).to eq '' + expect(note.noteable.time_spent).to eq(3600) + end + end end describe 'note with command & text' do diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index becf627a4f5..66fc8fc360b 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -1,12 +1,13 @@ require 'spec_helper' describe SlashCommands::InterpretService, services: true do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } let(:developer) { create(:user) } let(:issue) { create(:issue, project: project) } let(:milestone) { create(:milestone, project: project, title: '9.10') } let(:inprogress) { create(:label, project: project, title: 'In Progress') } let(:bug) { create(:label, project: project, title: 'Bug') } + let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } before do project.team << [developer, :developer] @@ -210,6 +211,46 @@ describe SlashCommands::InterpretService, services: true do end end + shared_examples 'estimate command' do + it 'populates time_estimate: 3600 if content contains /estimate 1h' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(time_estimate: 3600) + end + end + + shared_examples 'spend command' do + it 'populates spend_time: 3600 if content contains /spend 1h' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: { duration: 3600, user: developer }) + end + end + + shared_examples 'spend command with negative time' do + it 'populates spend_time: -1800 if content contains /spend -30m' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: { duration: -1800, user: developer }) + end + end + + shared_examples 'remove_estimate command' do + it 'populates time_estimate: 0 if content contains /remove_estimate' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(time_estimate: 0) + end + end + + shared_examples 'remove_time_spent command' do + it 'populates spend_time: :reset if content contains /remove_time_spent' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(spend_time: { duration: :reset, user: developer }) + end + end + shared_examples 'empty command' do it 'populates {} if content contains an unsupported command' do _, updates = service.execute(content, issuable) @@ -218,6 +259,14 @@ describe SlashCommands::InterpretService, services: true do end end + shared_examples 'merge command' do + it 'runs merge command if content contains /merge' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(merge: merge_request.diff_head_sha) + end + end + it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -238,6 +287,64 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { merge_request } end + context 'merge command' do + let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: merge_request.diff_head_sha }) } + + it_behaves_like 'merge command' do + let(:content) { '/merge' } + let(:issuable) { merge_request } + end + + context 'can not be merged when logged user does not have permissions' do + let(:service) { described_class.new(project, create(:user)) } + + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { merge_request } + end + end + + context 'can not be merged when sha does not match' do + let(:service) { described_class.new(project, developer, { merge_request_diff_head_sha: 'othersha' }) } + + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { merge_request } + end + end + + context 'when sha is missing' do + let(:service) { described_class.new(project, developer, {}) } + + it 'precheck passes and returns merge command' do + _, updates = service.execute('/merge', merge_request) + + expect(updates).to eq(merge: nil) + end + end + + context 'issue can not be merged' do + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { issue } + end + end + + context 'non persisted merge request cant be merged' do + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { build(:merge_request) } + end + end + + context 'not persisted merge request can not be merged' do + it_behaves_like 'empty command' do + let(:content) { "/merge" } + let(:issuable) { build(:merge_request, source_project: project) } + end + end + end + it_behaves_like 'title command' do let(:content) { '/title A brand new title' } let(:issuable) { issue } @@ -451,6 +558,51 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { merge_request } end + it_behaves_like 'estimate command' do + let(:content) { '/estimate 1h' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/estimate' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/estimate abc' } + let(:issuable) { issue } + end + + it_behaves_like 'spend command' do + let(:content) { '/spend 1h' } + let(:issuable) { issue } + end + + it_behaves_like 'spend command with negative time' do + let(:content) { '/spend -30m' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/spend' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/spend abc' } + let(:issuable) { issue } + end + + it_behaves_like 'remove_estimate command' do + let(:content) { '/remove_estimate' } + let(:issuable) { issue } + end + + it_behaves_like 'remove_time_spent command' do + let(:content) { '/remove_time_spent' } + let(:issuable) { issue } + end + context 'when current_user cannot :admin_issue' do let(:visitor) { create(:user) } let(:issue) { create(:issue, project: project, author: visitor) } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 0e8adb68721..9f5a0ac4ec6 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -740,4 +740,92 @@ describe SystemNoteService, services: true do expect(note.note).to include(issue.to_reference) end end + + describe '.change_time_estimate' do + subject { described_class.change_time_estimate(noteable, project, author) } + + it_behaves_like 'a system note' + + context 'with a time estimate' do + it 'sets the note text' do + noteable.update_attribute(:time_estimate, 277200) + + expect(subject.note).to eq "Changed time estimate of this issue to 1w 4d 5h" + end + end + + context 'without a time estimate' do + it 'sets the note text' do + expect(subject.note).to eq "Removed time estimate on this issue" + end + end + end + + describe '.change_time_spent' do + # We need a custom noteable in order to the shared examples to be green. + let(:noteable) do + mr = create(:merge_request, source_project: project) + mr.spend_time(duration: 360000, user: author) + mr.save! + mr + end + + subject do + described_class.change_time_spent(noteable, project, author) + end + + it_behaves_like 'a system note' + + context 'when time was added' do + it 'sets the note text' do + spend_time!(277200) + + expect(subject.note).to eq "Added 1w 4d 5h of time spent on this merge request" + end + end + + context 'when time was subtracted' do + it 'sets the note text' do + spend_time!(-277200) + + expect(subject.note).to eq "Subtracted 1w 4d 5h of time spent on this merge request" + end + end + + context 'when time was removed' do + it 'sets the note text' do + spend_time!(:reset) + + expect(subject.note).to eq "Removed time spent on this merge request" + end + end + + def spend_time!(seconds) + noteable.spend_time(duration: seconds, user: author) + noteable.save! + end + end + + describe '.add_merge_request_wip_from_commit' do + let(:noteable) do + create(:merge_request, source_project: project, target_project: project) + end + + subject do + described_class.add_merge_request_wip_from_commit( + noteable, + project, + author, + noteable.diff_head_commit + ) + end + + it_behaves_like 'a system note' + + it "posts the 'marked as a Work In Progress from commit' system note" do + expect(subject.note).to match( + /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/ + ) + end + end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index ed55791d24e..13d584a8975 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -469,6 +469,13 @@ describe TodoService, services: true do should_create_todo(user: author, target: mr_unassigned, action: Todo::BUILD_FAILED) end + + it 'creates a pending todo for merge_user' do + mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin) + service.merge_request_build_failed(mr_unassigned) + + should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::BUILD_FAILED) + end end describe '#merge_request_push' do @@ -482,6 +489,15 @@ describe TodoService, services: true do end end + describe '#merge_request_became_unmergeable' do + it 'creates a pending todo for a merge_user' do + mr_unassigned.update(merge_when_build_succeeds: true, merge_user: admin) + service.merge_request_became_unmergeable(mr_unassigned) + + should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE) + end + end + describe '#mark_todo' do it 'creates a todo from a merge request' do service.mark_todo(mr_unassigned, author) diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index 9fbb61565e3..690fe979492 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -10,7 +10,21 @@ describe Users::RefreshAuthorizedProjectsService do create!(project: project, user: user, access_level: access_level) end - describe '#execute' do + describe '#execute', :redis do + it 'refreshes the authorizations using a lease' do + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). + and_return('foo') + + expect(Gitlab::ExclusiveLease).to receive(:cancel). + with(an_instance_of(String), 'foo') + + expect(service).to receive(:execute_without_lease) + + service.execute + end + end + + describe '#execute_without_lease' do before do user.project_authorizations.delete_all end @@ -19,37 +33,23 @@ describe Users::RefreshAuthorizedProjectsService do project2 = create(:empty_project) to_remove = create_authorization(project2, user) - expect(service).to receive(:update_with_lease). + expect(service).to receive(:update_authorizations). with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) - service.execute + service.execute_without_lease end it 'sets the access level of a project to the highest available level' do to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER) - expect(service).to receive(:update_with_lease). + expect(service).to receive(:update_authorizations). with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) - service.execute + service.execute_without_lease end it 'returns a User' do - expect(service.execute).to be_an_instance_of(User) - end - end - - describe '#update_with_lease', :redis do - it 'refreshes the authorizations using a lease' do - expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). - and_return('foo') - - expect(Gitlab::ExclusiveLease).to receive(:cancel). - with(an_instance_of(String), 'foo') - - expect(service).to receive(:update_authorizations).with([1], []) - - service.update_with_lease([1]) + expect(service.execute_without_lease).to be_an_instance_of(User) end end diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb new file mode 100644 index 00000000000..210cd5817e0 --- /dev/null +++ b/spec/support/api/time_tracking_shared_examples.rb @@ -0,0 +1,132 @@ +shared_examples 'an unauthorized API user' do + it { is_expected.to eq(403) } +end + +shared_examples 'time tracking endpoints' do |issuable_name| + issuable_collection_name = issuable_name.pluralize + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do + context 'with an unauthorized user' do + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", non_member), duration: '1w') } + + it_behaves_like 'an unauthorized API user' + end + + it "sets the time estimate for #{issuable_name}" do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' + + expect(response).to have_http_status(200) + expect(json_response['human_time_estimate']).to eq('1w') + end + + describe 'updating the current estimate' do + before do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '1w' + end + + context 'when duration has a bad format' do + it 'does not modify the original estimate' do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: 'foo' + + expect(response).to have_http_status(400) + expect(issuable.reload.human_time_estimate).to eq('1w') + end + end + + context 'with a valid duration' do + it 'updates the estimate' do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_estimate", user), duration: '3w1h' + + expect(response).to have_http_status(200) + expect(issuable.reload.human_time_estimate).to eq('3w 1h') + end + end + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_time_estimate" do + context 'with an unauthorized user' do + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", non_member)) } + + it_behaves_like 'an unauthorized API user' + end + + it "resets the time estimate for #{issuable_name}" do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_time_estimate", user) + + expect(response).to have_http_status(200) + expect(json_response['time_estimate']).to eq(0) + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/add_spent_time" do + context 'with an unauthorized user' do + subject do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", non_member), + duration: '2h' + end + + it_behaves_like 'an unauthorized API user' + end + + it "add spent time for #{issuable_name}" do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + duration: '2h' + + expect(response).to have_http_status(201) + expect(json_response['human_total_time_spent']).to eq('2h') + end + + context 'when subtracting time' do + it 'subtracts time of the total spent time' do + issuable.update_attributes!(spend_time: { duration: 7200, user: user }) + + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + duration: '-1h' + + expect(response).to have_http_status(201) + expect(json_response['total_time_spent']).to eq(3600) + end + end + + context 'when time to subtract is greater than the total spent time' do + it 'does not modify the total time spent' do + issuable.update_attributes!(spend_time: { duration: 7200, user: user }) + + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user), + duration: '-1w' + + expect(response).to have_http_status(400) + expect(json_response['message']['time_spent'].first).to match(/exceeds the total time spent/) + end + end + end + + describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/reset_spent_time" do + context 'with an unauthorized user' do + subject { post(api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", non_member)) } + + it_behaves_like 'an unauthorized API user' + end + + it "resets spent time for #{issuable_name}" do + post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/reset_spent_time", user) + + expect(response).to have_http_status(200) + expect(json_response['total_time_spent']).to eq(0) + end + end + + describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do + it "returns the time stats for #{issuable_name}" do + issuable.update_attributes!(spend_time: { duration: 1800, user: user }, + time_estimate: 3600) + + get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user) + + expect(response).to have_http_status(200) + expect(json_response['total_time_spent']).to eq(1800) + expect(json_response['time_estimate']).to eq(3600) + end + end +end diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index 8e19a6c92e2..35b40d73191 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -2,7 +2,6 @@ # Note: The ABC size is large here because we have a method generating test cases with # multiple nested contexts. This shouldn't count as a violation. - module CycleAnalyticsHelpers module TestGeneration # Generate the most common set of specs that all cycle analytics phases need to have. @@ -51,7 +50,7 @@ module CycleAnalyticsHelpers end median_time_difference = time_differences.sort[2] - expect(subject.send(phase)).to be_within(5).of(median_time_difference) + expect(subject[phase].median).to be_within(5).of(median_time_difference) end context "when the data belongs to another project" do @@ -83,7 +82,7 @@ module CycleAnalyticsHelpers # Turn off the stub before checking assertions allow(self).to receive(:project).and_call_original - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end @@ -106,7 +105,7 @@ module CycleAnalyticsHelpers Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -126,7 +125,7 @@ module CycleAnalyticsHelpers Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn end - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -145,7 +144,7 @@ module CycleAnalyticsHelpers post_fn[self, data] if post_fn end - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -153,7 +152,7 @@ module CycleAnalyticsHelpers context "when none of the start / end conditions are matched" do it "returns nil" do - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 4cf81be3adc..90f1a9c8798 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -35,7 +35,8 @@ module TestEnv 'conflict-missing-side' => 'eb227b3', 'conflict-non-utf8' => 'd0a293c', 'conflict-too-large' => '39fa04f', - 'deleted-image-test' => '6c17798' + 'deleted-image-test' => '6c17798', + 'wip' => 'b9238ee' } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb new file mode 100644 index 00000000000..02657684b57 --- /dev/null +++ b/spec/support/time_tracking_shared_examples.rb @@ -0,0 +1,82 @@ +shared_examples 'issuable time tracker' do + it 'renders the sidebar component empty state' do + page.within '.time-tracking-no-tracking-pane' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'updates the sidebar component when estimate is added' do + submit_time('/estimate 3w 1d 1h') + + page.within '.time-tracking-estimate-only-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'updates the sidebar component when spent is added' do + submit_time('/spend 3w 1d 1h') + + page.within '.time-tracking-spend-only-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'shows the comparison when estimate and spent are added' do + submit_time('/estimate 3w 1d 1h') + submit_time('/spend 3w 1d 1h') + + page.within '.time-tracking-comparison-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'updates the sidebar component when estimate is removed' do + submit_time('/estimate 3w 1d 1h') + submit_time('/remove_estimate') + + page.within '#issuable-time-tracker' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'updates the sidebar component when spent is removed' do + submit_time('/spend 3w 1d 1h') + submit_time('/remove_time_spent') + + page.within '#issuable-time-tracker' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'shows the help state when icon is clicked' do + page.within '#issuable-time-tracker' do + find('.help-button').click + expect(page).to have_content 'Track time with slash commands' + expect(page).to have_content 'Learn more' + end + end + + it 'hides the help state when close icon is clicked' do + page.within '#issuable-time-tracker' do + find('.help-button').click + find('.close-help-button').click + + expect(page).not_to have_content 'Track time with slash commands' + expect(page).not_to have_content 'Learn more' + end + end + + it 'displays the correct help url' do + page.within '#issuable-time-tracker' do + find('.help-button').click + + expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md') + end + end +end + +def submit_time(slash_command) + fill_in 'note[note]', with: slash_command + click_button 'Comment' + wait_for_ajax +end |